from contextlib import AsyncExitStack
from logging import DEBUG, INFO, Logger
from os import chmod, geteuid
from pathlib import Path
from tempfile import NamedTemporaryFile, TemporaryDirectory
from typing import Any, Callable
from unittest import IsolatedAsyncioTestCase, main
from unittest.mock import AsyncMock, MagicMock, call, patch
from uuid import uuid4

from async_helpers import run_async

from avalan.agent.loader import OrchestratorLoader
from avalan.entities import (
    OrchestratorSettings,
    PermanentMemoryStoreSettings,
    ToolCallRecoveryFormat,
    ToolFormat,
)
from avalan.event import Event, EventType
from avalan.model.hubs.huggingface import HuggingfaceHub
from avalan.tool.browser import BrowserToolSettings
from avalan.tool.context import ToolSettingsContext
from avalan.tool.database import DatabaseToolSettings
from avalan.tool.graph_settings import GraphToolSettings


class LoaderPropertyTestCase(IsolatedAsyncioTestCase):
    async def test_hub_and_participant_id(self):
        stack = AsyncExitStack()
        hub = MagicMock(spec=HuggingfaceHub)
        logger = MagicMock(spec=Logger)
        participant_id = uuid4()
        loader = OrchestratorLoader(
            hub=hub, logger=logger, participant_id=participant_id, stack=stack
        )
        self.assertIs(loader.hub, hub)
        self.assertEqual(loader.participant_id, participant_id)
        await stack.aclose()


class LoaderFromFileTestCase(IsolatedAsyncioTestCase):
    async def test_file_not_found(self):
        stack = AsyncExitStack()
        loader = OrchestratorLoader(
            hub=MagicMock(spec=HuggingfaceHub),
            logger=MagicMock(spec=Logger),
            participant_id=uuid4(),
            stack=stack,
        )
        with self.assertRaises(FileNotFoundError):
            await loader.from_file("missing.toml", agent_id=uuid4())
        await stack.aclose()

    async def test_permission_error(self):
        if geteuid() == 0:
            self.skipTest("Running as root; permission error won't occur")
        with NamedTemporaryFile(delete=False) as tmp:
            path = tmp.name
        chmod(path, 0)
        stack = AsyncExitStack()
        try:
            loader = OrchestratorLoader(
                hub=MagicMock(spec=HuggingfaceHub),
                logger=MagicMock(spec=Logger),
                participant_id=uuid4(),
                stack=stack,
            )
            with self.assertRaises(PermissionError):
                await loader.from_file(path, agent_id=uuid4())
        finally:
            chmod(path, 0o644)
        await stack.aclose()

    async def test_permission_error_when_access_denied(self):
        with NamedTemporaryFile() as tmp:
            path = tmp.name
        stack = AsyncExitStack()
        with (
            patch("avalan.agent.loader.exists", return_value=True),
            patch("avalan.agent.loader.access", return_value=False),
        ):
            loader = OrchestratorLoader(
                hub=MagicMock(spec=HuggingfaceHub),
                logger=MagicMock(spec=Logger),
                participant_id=uuid4(),
                stack=stack,
            )
            with self.assertRaises(PermissionError):
                await loader.from_file(path, agent_id=uuid4())
        await stack.aclose()

    def test_validate_agent_file_permission_error_when_access_denied(self):
        with NamedTemporaryFile() as tmp:
            path = tmp.name
        with (
            patch("avalan.agent.loader.exists", return_value=True),
            patch("avalan.agent.loader.access", return_value=False),
        ):
            with self.assertRaises(PermissionError):
                run_async(OrchestratorLoader.validate_agent_file(path))

    def test_validate_agent_file_returns_config_and_missing_file(self):
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write("""
[agent]
role = \"assistant\"

[engine]
uri = \"ai://local/model\"
""")

            config = run_async(OrchestratorLoader.validate_agent_file(path))

        self.assertEqual(config["engine"]["uri"], "ai://local/model")
        with self.assertRaises(FileNotFoundError):
            run_async(
                OrchestratorLoader.validate_agent_file("missing-agent.toml")
            )

    def test_validate_agent_config_rejects_conflicting_user_templates(self):
        with self.assertRaises(AssertionError):
            OrchestratorLoader.validate_agent_config(
                {
                    "agent": {
                        "user": "literal",
                        "user_template": "template",
                    },
                    "engine": {"uri": "ai://local/model"},
                }
            )
        with self.assertRaises(AssertionError):
            OrchestratorLoader.validate_agent_config(
                {
                    "agent": {"type": "invalid"},
                    "engine": {"uri": "ai://local/model"},
                }
            )

    def test_validate_agent_config_rejects_non_string_direct_prompts(self):
        for key in (
            "system",
            "developer",
            "instructions",
            "goal_instructions",
            "user",
            "user_template",
        ):
            with self.subTest(key=key):
                with self.assertRaises(AssertionError):
                    OrchestratorLoader.validate_agent_config(
                        {
                            "agent": {key: 1},
                            "engine": {"uri": "ai://local/model"},
                        }
                    )

    async def test_load_default_orchestrator(self):
        config = """
[agent]
role = \"assistant\"
task = \"do\"
goal_instructions = \"how\"

[engine]
uri = \"ai://local/model\"
"""
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            hub = MagicMock(spec=HuggingfaceHub)
            logger = MagicMock(spec=Logger)
            stack = AsyncExitStack()

            sentence_model = MagicMock()
            sentence_model.__enter__.return_value = sentence_model

            model_manager = MagicMock()
            model_manager.__enter__.return_value = model_manager
            model_manager.parse_uri.return_value = "uri_obj"
            model_manager.get_engine_settings.return_value = "settings_obj"

            memory = MagicMock()
            tool = MagicMock()
            browser_toolset = MagicMock()
            event_manager = MagicMock()

            with (
                patch(
                    "avalan.agent.loader.SentenceTransformerModel",
                    return_value=sentence_model,
                ),
                patch("avalan.agent.loader.TextPartitioner"),
                patch(
                    "avalan.agent.loader.MemoryManager.create_instance",
                    new=AsyncMock(return_value=memory),
                ) as mm_patch,
                patch(
                    "avalan.agent.loader.ModelManager",
                    return_value=model_manager,
                ) as model_patch,
                patch(
                    "avalan.agent.loader.DefaultOrchestrator",
                    return_value="orch",
                ) as orch_patch,
                patch(
                    "avalan.agent.loader.ToolManager.create_instance",
                    return_value=tool,
                ),
                patch(
                    "avalan.agent.loader.BrowserToolSet",
                    return_value=browser_toolset,
                ) as bts_patch,
                patch(
                    "avalan.agent.loader.EventManager",
                    return_value=event_manager,
                ),
            ):
                loader = OrchestratorLoader(
                    hub=hub,
                    logger=logger,
                    participant_id=uuid4(),
                    stack=stack,
                )
                result = await loader.from_file(
                    path,
                    agent_id=uuid4(),
                    disable_memory=True,
                )

                self.assertEqual(result, "orch")
                orch_patch.assert_called_once()
                model_patch.assert_called_once_with(
                    hub, logger, event_manager=event_manager
                )
                mm_patch.assert_awaited_once()
                bts = bts_patch.call_args.kwargs["settings"]
                self.assertIsInstance(bts, BrowserToolSettings)
                self.assertEqual(bts.engine, "firefox")
            await stack.aclose()

    async def test_load_tool_settings(self):
        config = """
[agent]
role = \"assistant\"

[engine]
uri = \"ai://local/model\"

[tool.browser.open]
engine = \"webkit\"
debug = true
"""
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            hub = MagicMock(spec=HuggingfaceHub)
            logger = MagicMock(spec=Logger)
            stack = AsyncExitStack()

            sentence_model = MagicMock()
            sentence_model.__enter__.return_value = sentence_model

            model_manager = MagicMock()
            model_manager.__enter__.return_value = model_manager
            model_manager.parse_uri.return_value = "uri_obj"
            model_manager.get_engine_settings.return_value = "settings_obj"

            memory = MagicMock()
            tool = MagicMock()
            browser_toolset = MagicMock()
            event_manager = MagicMock()

            with (
                patch(
                    "avalan.agent.loader.SentenceTransformerModel",
                    return_value=sentence_model,
                ),
                patch("avalan.agent.loader.TextPartitioner"),
                patch(
                    "avalan.agent.loader.MemoryManager.create_instance",
                    new=AsyncMock(return_value=memory),
                ),
                patch(
                    "avalan.agent.loader.ModelManager",
                    return_value=model_manager,
                ),
                patch(
                    "avalan.agent.loader.DefaultOrchestrator",
                    return_value="orch",
                ),
                patch(
                    "avalan.agent.loader.ToolManager.create_instance",
                    return_value=tool,
                ),
                patch(
                    "avalan.agent.loader.BrowserToolSet",
                    return_value=browser_toolset,
                ) as bts_patch,
                patch(
                    "avalan.agent.loader.EventManager",
                    return_value=event_manager,
                ),
            ):
                loader = OrchestratorLoader(
                    hub=hub,
                    logger=logger,
                    participant_id=uuid4(),
                    stack=stack,
                )
                await loader.from_file(
                    path,
                    agent_id=uuid4(),
                    disable_memory=True,
                )

                bs = bts_patch.call_args.kwargs["settings"]
                self.assertIsInstance(bs, BrowserToolSettings)
                self.assertEqual(bs.engine, "webkit")
                self.assertTrue(bs.debug)
            await stack.aclose()

    async def test_load_old_tool_settings_section(self):
        config = """
[agent]
role = \"assistant\"

[engine]
uri = \"ai://local/model\"

[tool.browser]
engine = \"chromium\"
"""
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            hub = MagicMock(spec=HuggingfaceHub)
            logger = MagicMock(spec=Logger)
            stack = AsyncExitStack()

            sentence_model = MagicMock()
            sentence_model.__enter__.return_value = sentence_model

            model_manager = MagicMock()
            model_manager.__enter__.return_value = model_manager
            model_manager.parse_uri.return_value = "uri_obj"
            model_manager.get_engine_settings.return_value = "settings_obj"

            memory = MagicMock()
            tool = MagicMock()
            browser_toolset = MagicMock()
            event_manager = MagicMock()

            with (
                patch(
                    "avalan.agent.loader.SentenceTransformerModel",
                    return_value=sentence_model,
                ),
                patch("avalan.agent.loader.TextPartitioner"),
                patch(
                    "avalan.agent.loader.MemoryManager.create_instance",
                    new=AsyncMock(return_value=memory),
                ),
                patch(
                    "avalan.agent.loader.ModelManager",
                    return_value=model_manager,
                ),
                patch(
                    "avalan.agent.loader.DefaultOrchestrator",
                    return_value="orch",
                ),
                patch(
                    "avalan.agent.loader.ToolManager.create_instance",
                    return_value=tool,
                ),
                patch(
                    "avalan.agent.loader.BrowserToolSet",
                    return_value=browser_toolset,
                ) as bts_patch,
                patch(
                    "avalan.agent.loader.EventManager",
                    return_value=event_manager,
                ),
            ):
                loader = OrchestratorLoader(
                    hub=hub,
                    logger=logger,
                    participant_id=uuid4(),
                    stack=stack,
                )
                await loader.from_file(
                    path,
                    agent_id=uuid4(),
                    disable_memory=True,
                )

                bs = bts_patch.call_args.kwargs["settings"]
                self.assertIsInstance(bs, BrowserToolSettings)
                self.assertEqual(bs.engine, "chromium")
            await stack.aclose()

    async def test_load_database_tool_settings(self):
        config = """
[agent]
role = \"assistant\"

[engine]
uri = \"ai://local/model\"

[tool.database]
dsn = \"sqlite:///db.sqlite\"
"""
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            hub = MagicMock(spec=HuggingfaceHub)
            logger = MagicMock(spec=Logger)
            stack = AsyncExitStack()

            with patch.object(
                OrchestratorLoader,
                "from_settings",
                new=AsyncMock(return_value="orch"),
            ) as lfs_patch:
                loader = OrchestratorLoader(
                    hub=hub,
                    logger=logger,
                    participant_id=uuid4(),
                    stack=stack,
                )
                await loader.from_file(
                    path,
                    agent_id=uuid4(),
                )

                tool_settings = lfs_patch.call_args.kwargs["tool_settings"]
                dbs = tool_settings.database
                self.assertIsInstance(dbs, DatabaseToolSettings)
                self.assertEqual(dbs.dsn, "sqlite:///db.sqlite")
            await stack.aclose()

    async def test_load_graph_tool_settings(self):
        config = """
[agent]
role = \"assistant\"

[engine]
uri = \"ai://local/model\"

[tool.graph]
file = \"/tmp/chart.png\"
"""
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            hub = MagicMock(spec=HuggingfaceHub)
            logger = MagicMock(spec=Logger)
            stack = AsyncExitStack()

            with patch.object(
                OrchestratorLoader,
                "from_settings",
                new=AsyncMock(return_value="orch"),
            ) as lfs_patch:
                loader = OrchestratorLoader(
                    hub=hub,
                    logger=logger,
                    participant_id=uuid4(),
                    stack=stack,
                )
                await loader.from_file(path, agent_id=uuid4())

                tool_settings = lfs_patch.call_args.kwargs["tool_settings"]
                graph = tool_settings.graph
                self.assertIsInstance(graph, GraphToolSettings)
                self.assertEqual(graph.file, "/tmp/chart.png")
            await stack.aclose()

    async def test_tool_format_setting(self):
        config = """
[agent]
role = \"assistant\"

[engine]
uri = \"ai://local/model\"

[tool]
format = \"react\"
"""
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            hub = MagicMock(spec=HuggingfaceHub)
            logger = MagicMock(spec=Logger)
            stack = AsyncExitStack()

            sentence_model = MagicMock()
            sentence_model.__enter__.return_value = sentence_model

            model_manager = MagicMock()
            model_manager.__enter__.return_value = model_manager
            model_manager.parse_uri.return_value = "uri_obj"
            model_manager.get_engine_settings.return_value = "settings_obj"

            memory = MagicMock()
            tool = MagicMock()
            browser_toolset = MagicMock()
            event_manager = MagicMock()

            with (
                patch(
                    "avalan.agent.loader.SentenceTransformerModel",
                    return_value=sentence_model,
                ),
                patch("avalan.agent.loader.TextPartitioner"),
                patch(
                    "avalan.agent.loader.MemoryManager.create_instance",
                    new=AsyncMock(return_value=memory),
                ),
                patch(
                    "avalan.agent.loader.ModelManager",
                    return_value=model_manager,
                ),
                patch(
                    "avalan.agent.loader.DefaultOrchestrator",
                    return_value="orch",
                ),
                patch(
                    "avalan.agent.loader.ToolManager.create_instance",
                    return_value=tool,
                ) as tm_patch,
                patch(
                    "avalan.agent.loader.BrowserToolSet",
                    return_value=browser_toolset,
                ),
                patch(
                    "avalan.agent.loader.EventManager",
                    return_value=event_manager,
                ),
            ):
                loader = OrchestratorLoader(
                    hub=hub,
                    logger=logger,
                    participant_id=uuid4(),
                    stack=stack,
                )
                await loader.from_file(path, agent_id=uuid4())

                settings = tm_patch.call_args.kwargs["settings"]
                self.assertEqual(settings.tool_format, ToolFormat.REACT)
            await stack.aclose()

    async def test_tool_enable_accepts_list(self):
        config = """
[agent]
role = \"assistant\"

[engine]
uri = \"ai://local/model\"

[tool]
enable = [\"math.calculator\", \"browser.open\"]
"""
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            stack = AsyncExitStack()
            hub = MagicMock(spec=HuggingfaceHub)
            logger = MagicMock(spec=Logger)

            with patch.object(
                OrchestratorLoader,
                "from_settings",
                new=AsyncMock(return_value="orch"),
            ) as from_settings:
                loader = OrchestratorLoader(
                    hub=hub,
                    logger=logger,
                    participant_id=uuid4(),
                    stack=stack,
                )
                result = await loader.from_file(path, agent_id=uuid4())

                self.assertEqual(result, "orch")
                settings = from_settings.call_args.args[0]
                self.assertEqual(
                    settings.tools, ["math.calculator", "browser.open"]
                )
            await stack.aclose()

    async def test_tool_enable_accepts_string(self):
        config = """
[agent]
role = \"assistant\"

[engine]
uri = \"ai://local/model\"

[tool]
enable = \"math.calculator\"
"""
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            stack = AsyncExitStack()
            hub = MagicMock(spec=HuggingfaceHub)
            logger = MagicMock(spec=Logger)

            with patch.object(
                OrchestratorLoader,
                "from_settings",
                new=AsyncMock(return_value="orch"),
            ) as from_settings:
                loader = OrchestratorLoader(
                    hub=hub,
                    logger=logger,
                    participant_id=uuid4(),
                    stack=stack,
                )
                await loader.from_file(path, agent_id=uuid4())

                settings = from_settings.call_args.args[0]
                self.assertEqual(settings.tools, ["math.calculator"])
            await stack.aclose()

    async def test_tool_enable_invalid_type(self):
        config = """
[agent]
role = \"assistant\"

[engine]
uri = \"ai://local/model\"

[tool]
enable = 3
"""
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            stack = AsyncExitStack()
            hub = MagicMock(spec=HuggingfaceHub)
            logger = MagicMock(spec=Logger)

            loader = OrchestratorLoader(
                hub=hub,
                logger=logger,
                participant_id=uuid4(),
                stack=stack,
            )

            with self.assertRaises(AssertionError):
                await loader.from_file(path, agent_id=uuid4())

            await stack.aclose()

    async def test_engine_section_tools_option_not_supported(self):
        config = """
[agent]
role = \"assistant\"

[engine]
uri = \"ai://local/model\"
tools = [\"math.calculator\"]
"""
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            stack = AsyncExitStack()
            hub = MagicMock(spec=HuggingfaceHub)
            logger = MagicMock(spec=Logger)

            loader = OrchestratorLoader(
                hub=hub,
                logger=logger,
                participant_id=uuid4(),
                stack=stack,
            )

            with self.assertRaises(AssertionError):
                await loader.from_file(path, agent_id=uuid4())

            await stack.aclose()

    async def test_tool_format_variants(self):
        for value, expected in (
            ("react", ToolFormat.REACT),
            ("json", ToolFormat.JSON),
            ("openai", ToolFormat.OPENAI),
            ("dsml", ToolFormat.DSML),
        ):
            with self.subTest(value=value):
                config = f"""
[agent]
role = \"assistant\"

[engine]
uri = \"ai://local/model\"

[tool]
format = \"{value}\"
"""
                with TemporaryDirectory() as tmp:
                    path = f"{tmp}/agent.toml"
                    with open(path, "w", encoding="utf-8") as fh:
                        fh.write(config)

                    stack = AsyncExitStack()
                    hub = MagicMock(spec=HuggingfaceHub)
                    logger = MagicMock(spec=Logger)

                    with patch.object(
                        OrchestratorLoader,
                        "from_settings",
                        new=AsyncMock(return_value="orch"),
                    ) as from_settings:
                        loader = OrchestratorLoader(
                            hub=hub,
                            logger=logger,
                            participant_id=uuid4(),
                            stack=stack,
                        )
                        await loader.from_file(path, agent_id=uuid4())

                        tool_format = from_settings.call_args.kwargs[
                            "tool_format"
                        ]
                        self.assertEqual(tool_format, expected)
                    await stack.aclose()

    async def test_tool_recovery_format_variants(self):
        config = """
[agent]
role = \"assistant\"

[engine]
uri = \"ai://local/model\"

[tool]
recovery_formats = [\"fenced\", \"tool_call_block\"]
"""
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            stack = AsyncExitStack()
            hub = MagicMock(spec=HuggingfaceHub)
            logger = MagicMock(spec=Logger)

            with patch.object(
                OrchestratorLoader,
                "from_settings",
                new=AsyncMock(return_value="orch"),
            ) as from_settings:
                loader = OrchestratorLoader(
                    hub=hub,
                    logger=logger,
                    participant_id=uuid4(),
                    stack=stack,
                )
                await loader.from_file(path, agent_id=uuid4())

                self.assertEqual(
                    from_settings.call_args.kwargs["tool_recovery_formats"],
                    [
                        ToolCallRecoveryFormat.FENCED,
                        ToolCallRecoveryFormat.TOOL_CALL_BLOCK,
                    ],
                )
            await stack.aclose()

    async def test_tool_recovery_formats_reject_non_list(self):
        config = """
[agent]
role = \"assistant\"

[engine]
uri = \"ai://local/model\"

[tool]
recovery_formats = \"fenced\"
"""
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            stack = AsyncExitStack()
            hub = MagicMock(spec=HuggingfaceHub)
            logger = MagicMock(spec=Logger)

            loader = OrchestratorLoader(
                hub=hub,
                logger=logger,
                participant_id=uuid4(),
                stack=stack,
            )
            with self.assertRaises(AssertionError):
                await loader.from_file(path, agent_id=uuid4())
            await stack.aclose()

    async def test_tool_recovery_formats_reject_unknown_value(self):
        config = """
[agent]
role = \"assistant\"

[engine]
uri = \"ai://local/model\"

[tool]
recovery_formats = [\"unknown\"]
"""
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            stack = AsyncExitStack()
            hub = MagicMock(spec=HuggingfaceHub)
            logger = MagicMock(spec=Logger)

            loader = OrchestratorLoader(
                hub=hub,
                logger=logger,
                participant_id=uuid4(),
                stack=stack,
            )
            with self.assertRaises(ValueError):
                await loader.from_file(path, agent_id=uuid4())
            await stack.aclose()

    async def test_tool_settings_argument(self):
        config = """
[agent]
role = \"assistant\"

[engine]
uri = \"ai://local/model\"
"""
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            hub = MagicMock(spec=HuggingfaceHub)
            logger = MagicMock(spec=Logger)
            stack = AsyncExitStack()

            tool_settings = ToolSettingsContext(
                browser=BrowserToolSettings(engine="webkit"),
                database=DatabaseToolSettings(dsn="sqlite:///db.sqlite"),
                graph=GraphToolSettings(file="/tmp/chart.png"),
                extra={"x": 1},
            )

            with patch.object(
                OrchestratorLoader,
                "from_settings",
                new=AsyncMock(return_value="orch"),
            ) as fs_patch:
                loader = OrchestratorLoader(
                    hub=hub,
                    logger=logger,
                    participant_id=uuid4(),
                    stack=stack,
                )
                result = await loader.from_file(
                    path, agent_id=uuid4(), tool_settings=tool_settings
                )
                self.assertEqual(result, "orch")
                fs_patch.assert_awaited_once()
                passed = fs_patch.call_args.kwargs["tool_settings"]
                self.assertEqual(passed.browser.engine, "webkit")
                self.assertEqual(passed.database.dsn, "sqlite:///db.sqlite")
                self.assertEqual(passed.graph.file, "/tmp/chart.png")
                self.assertEqual(passed.extra, {"x": 1})
            await stack.aclose()

    async def test_load_json_orchestrator(self):
        config = """
[agent]
type = \"json\"
role = \"assistant\"
task = \"do\"
goal_instructions = \"how\"

[engine]
uri = \"ai://local/model\"

[json]
value = { type = \"string\", description = \"d\" }
"""
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            hub = MagicMock(spec=HuggingfaceHub)
            logger = MagicMock(spec=Logger)
            stack = AsyncExitStack()

            sentence_model = MagicMock()
            sentence_model.__enter__.return_value = sentence_model

            model_manager = MagicMock()
            model_manager.__enter__.return_value = model_manager
            model_manager.parse_uri.return_value = "uri_obj"
            model_manager.get_engine_settings.return_value = "settings_obj"

            memory = MagicMock()
            tool = MagicMock()
            event_manager = MagicMock()

            with (
                patch(
                    "avalan.agent.loader.SentenceTransformerModel",
                    return_value=sentence_model,
                ),
                patch("avalan.agent.loader.TextPartitioner"),
                patch(
                    "avalan.agent.loader.MemoryManager.create_instance",
                    new=AsyncMock(return_value=memory),
                ),
                patch(
                    "avalan.agent.loader.ModelManager",
                    return_value=model_manager,
                ),
                patch.object(
                    OrchestratorLoader,
                    "_load_json_orchestrator",
                    return_value="json_orch",
                ) as json_patch,
                patch(
                    "avalan.agent.loader.ToolManager.create_instance",
                    return_value=tool,
                ),
                patch(
                    "avalan.agent.loader.EventManager",
                    return_value=event_manager,
                ),
            ):
                loader = OrchestratorLoader(
                    hub=hub,
                    logger=logger,
                    participant_id=uuid4(),
                    stack=stack,
                )
                result = await loader.from_file(
                    path,
                    agent_id=uuid4(),
                    disable_memory=True,
                )

                self.assertEqual(result, "json_orch")
                json_patch.assert_called_once()
            await stack.aclose()

    async def test_unknown_type(self):
        config = """
[agent]
type = \"foo\"
role = \"assistant\"

[engine]
uri = \"ai://local/model\"
"""
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            stack = AsyncExitStack()
            loader = OrchestratorLoader(
                hub=MagicMock(spec=HuggingfaceHub),
                logger=MagicMock(spec=Logger),
                participant_id=uuid4(),
                stack=stack,
            )
            with self.assertRaises(AssertionError):
                await loader.from_file(path, agent_id=uuid4())
        await stack.aclose()

    async def test_from_file_overrides_uri(self):
        config = """
[agent]
role = \"assistant\"

[engine]
uri = \"ai://old/model\"
"""
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            hub = MagicMock(spec=HuggingfaceHub)
            logger = MagicMock(spec=Logger)
            stack = AsyncExitStack()
            loader = OrchestratorLoader(
                hub=hub,
                logger=logger,
                participant_id=uuid4(),
                stack=stack,
            )
            with patch.object(
                loader,
                "from_settings",
                AsyncMock(return_value=MagicMock()),
            ) as fs:
                await loader.from_file(
                    path, agent_id=uuid4(), uri="ai://new/model"
                )
            settings_arg = fs.await_args.args[0]
            self.assertEqual(settings_arg.uri, "ai://new/model")
        await stack.aclose()

    async def test_sentence_model_engine_config(self):
        config = """
[agent]
role = \"assistant\"

[engine]
uri = \"ai://local/model\"

["memory.engine"]
model_id = "smodel"
max_tokens = 300
overlap_size = 60
window_size = 120
backend = "onnx"
"""
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            hub = MagicMock(spec=HuggingfaceHub)
            logger = MagicMock(spec=Logger)
            stack = AsyncExitStack()

            with patch.object(
                OrchestratorLoader,
                "from_settings",
                new=AsyncMock(return_value="orch"),
            ) as lfs_patch:
                loader = OrchestratorLoader(
                    hub=hub,
                    logger=logger,
                    participant_id=uuid4(),
                    stack=stack,
                )
                result = await loader.from_file(
                    path,
                    agent_id=uuid4(),
                )

                self.assertEqual(result, "orch")
                lfs_patch.assert_awaited_once()
                settings = lfs_patch.call_args.args[0]
                self.assertEqual(settings.sentence_model_id, "smodel")
                self.assertEqual(settings.sentence_model_max_tokens, 300)
                self.assertEqual(settings.sentence_model_overlap_size, 60)
                self.assertEqual(settings.sentence_model_window_size, 120)
                self.assertEqual(
                    settings.sentence_model_engine_config, {"backend": "onnx"}
                )
            await stack.aclose()


class LoaderTomlVariantsTestCase(IsolatedAsyncioTestCase):
    async def _run_loader(self, config: str) -> dict:
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            hub = MagicMock(spec=HuggingfaceHub)
            logger = MagicMock(spec=Logger)
            stack = AsyncExitStack()

            sentence_model = MagicMock()
            sentence_model.__enter__.return_value = sentence_model

            model_manager = MagicMock()
            model_manager.__enter__.return_value = model_manager
            model_manager.parse_uri.return_value = "uri_obj"
            model_manager.get_engine_settings.return_value = "settings_obj"

            memory = MagicMock()
            tool = MagicMock()
            browser_toolset = MagicMock()
            event_manager = MagicMock()

            with (
                patch(
                    "avalan.agent.loader.SentenceTransformerModel",
                    return_value=sentence_model,
                ),
                patch("avalan.agent.loader.TextPartitioner"),
                patch(
                    "avalan.agent.loader.MemoryManager.create_instance",
                    new=AsyncMock(return_value=memory),
                ),
                patch(
                    "avalan.agent.loader.ModelManager",
                    return_value=model_manager,
                ),
                patch(
                    "avalan.agent.loader.DefaultOrchestrator",
                    return_value="orch",
                ) as orch_patch,
                patch(
                    "avalan.agent.loader.ToolManager.create_instance",
                    return_value=tool,
                ),
                patch(
                    "avalan.agent.loader.BrowserToolSet",
                    return_value=browser_toolset,
                ),
                patch(
                    "avalan.agent.loader.EventManager",
                    return_value=event_manager,
                ),
            ):
                loader = OrchestratorLoader(
                    hub=hub,
                    logger=logger,
                    participant_id=uuid4(),
                    stack=stack,
                )
                await loader.from_file(
                    path,
                    agent_id=uuid4(),
                    disable_memory=True,
                )

                kwargs = orch_patch.call_args.kwargs
            await stack.aclose()
            return kwargs

    async def test_agent_system_only(self):
        config = """
[agent]
system = \"sys\"

[engine]
uri = \"ai://local/model\"
"""
        kwargs = await self._run_loader(config)
        self.assertEqual(kwargs["system"], "sys")
        self.assertIsNone(kwargs["role"])
        self.assertIsNone(kwargs["task"])
        self.assertIsNone(kwargs["instructions"])
        self.assertIsNone(kwargs["rules"])

    async def test_agent_developer_only(self):
        config = """
[agent]
developer = \"dev\"

[engine]
uri = \"ai://local/model\"
"""
        kwargs = await self._run_loader(config)
        self.assertEqual(kwargs["developer"], "dev")
        self.assertIsNone(kwargs["system"])
        self.assertIsNone(kwargs["role"])
        self.assertIsNone(kwargs["task"])
        self.assertIsNone(kwargs["instructions"])

    async def test_agent_system_and_developer_only(self):
        config = """
[agent]
system = \"sys\"
developer = \"dev\"

[engine]
uri = \"ai://local/model\"
"""
        kwargs = await self._run_loader(config)
        self.assertEqual(kwargs["system"], "sys")
        self.assertEqual(kwargs["developer"], "dev")
        self.assertIsNone(kwargs["role"])
        self.assertIsNone(kwargs["task"])
        self.assertIsNone(kwargs["instructions"])

    async def test_agent_system_developer_and_user_prompts(self):
        config = """
[agent]
system = \"sys\"
developer = \"dev\"
user = \"prefix\"
role = \"ignored\"
task = \"ignored\"
instructions = \"provider\"

[engine]
uri = \"ai://local/model\"
"""
        kwargs = await self._run_loader(config)
        self.assertEqual(kwargs["system"], "sys")
        self.assertEqual(kwargs["developer"], "dev")
        self.assertEqual(kwargs["user"], "prefix")
        self.assertIsNone(kwargs["role"])
        self.assertIsNone(kwargs["task"])
        self.assertEqual(kwargs["instructions"], "provider")

    async def test_agent_direct_prompt_invalid_type_rejected(self):
        config = """
[agent]
system = 1

[engine]
uri = \"ai://local/model\"
"""
        with self.assertRaises(AssertionError):
            await self._run_loader(config)

    async def test_agent_goal_instructions_invalid_type_rejected(self):
        config = """
[agent]
task = \"do\"
goal_instructions = 1

[engine]
uri = \"ai://local/model\"
"""
        with self.assertRaisesRegex(
            AssertionError, "agent.goal_instructions must be a string"
        ):
            await self._run_loader(config)

    async def test_agent_role_task_only(self):
        config = """
[agent]
role = \"assistant\"
task = \"do\"

[engine]
uri = \"ai://local/model\"
"""
        kwargs = await self._run_loader(config)
        self.assertEqual(kwargs["role"], "assistant")
        self.assertEqual(kwargs["task"], "do")
        self.assertIsNone(kwargs["instructions"])
        self.assertIsNone(kwargs["system"])

    async def test_agent_full_definition(self):
        config = """
[agent]
role = \"assistant\"
task = \"do\"
goal_instructions = \"how\"
rules = [\"r1\", \"r2\"]

[engine]
uri = \"ai://local/model\"
"""
        kwargs = await self._run_loader(config)
        self.assertEqual(kwargs["role"], "assistant")
        self.assertEqual(kwargs["task"], "do")
        self.assertEqual(kwargs["goal_instructions"], "how")
        self.assertIsNone(kwargs["instructions"])
        self.assertEqual(kwargs["rules"], ["r1", "r2"])
        self.assertIsNone(kwargs["system"])

    async def test_agent_rejects_old_goal_instructions_field(self):
        config = """
[agent]
role = \"assistant\"
task = \"do\"
instructions = \"how\"

[engine]
uri = \"ai://local/model\"
"""
        with self.assertRaisesRegex(
            AssertionError,
            "agent.instructions is reserved.*agent.goal_instructions",
        ):
            await self._run_loader(config)

    async def test_agent_rejects_goal_instructions_without_task(self):
        config = """
[agent]
role = \"assistant\"
goal_instructions = \"how\"

[engine]
uri = \"ai://local/model\"
"""
        with self.assertRaisesRegex(
            AssertionError, "agent.goal_instructions requires agent.task"
        ):
            await self._run_loader(config)

    async def test_agent_user_only(self):
        config = """
[agent]
user = \"hello {{input}}\"

[engine]
uri = \"ai://local/model\"
"""
        kwargs = await self._run_loader(config)
        self.assertEqual(kwargs["user"], "hello {{input}}")
        self.assertIsNone(kwargs.get("user_template"))

    async def test_agent_user_template_only(self):
        config = """
[agent]
user_template = \"u.md\"

[engine]
uri = \"ai://local/model\"
"""
        kwargs = await self._run_loader(config)
        self.assertEqual(kwargs["user_template"], "u.md")
        self.assertIsNone(kwargs.get("user"))

    async def test_agent_provider_instructions_and_user_prompt(self):
        config = """
[agent]
instructions = \"provider\"
user = \"prefix {{input}}\"

[engine]
uri = \"ai://local/model\"
"""
        kwargs = await self._run_loader(config)
        self.assertEqual(kwargs["instructions"], "provider")
        self.assertEqual(kwargs["user"], "prefix {{input}}")
        self.assertIsNone(kwargs["role"])
        self.assertIsNone(kwargs["task"])
        self.assertIsNone(kwargs["goal_instructions"])


class LoadJsonOrchestratorVariantsTestCase(IsolatedAsyncioTestCase):
    def test_system_only(self):
        agent_id = uuid4()
        engine_uri = MagicMock()
        engine_settings = MagicMock()
        logger = MagicMock()
        model_manager = MagicMock()
        memory = MagicMock()
        tool = MagicMock()
        event_manager = MagicMock()

        config = {"json": {"value": {"type": "string", "description": "v"}}}
        agent_config = {"system": "sys"}

        with patch("avalan.agent.loader.JsonOrchestrator") as orch_patch:
            OrchestratorLoader._load_json_orchestrator(
                agent_id=agent_id,
                engine_uri=engine_uri,
                engine_settings=engine_settings,
                logger=logger,
                model_manager=model_manager,
                memory=memory,
                tool=tool,
                event_manager=event_manager,
                config=config,
                agent_config=agent_config,
                call_options=None,
                template_vars=None,
            )
            kwargs = orch_patch.call_args.kwargs
            self.assertEqual(kwargs["system"], "sys")
            self.assertIsNone(kwargs["task"])
            self.assertIsNone(kwargs["instructions"])

    async def test_browser_debug_source(self):
        with TemporaryDirectory() as tmp:
            debug_path = f"{tmp}/debug.txt"
            with open(debug_path, "w", encoding="utf-8") as fh:
                fh.write("debug")

            config = f"""
[agent]
role = \"assistant\"

[engine]
uri = \"ai://local/model\"

[tool.browser.open]
debug = true
debug_source = \"{debug_path}\"
"""
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            hub = MagicMock(spec=HuggingfaceHub)
            logger = MagicMock(spec=Logger)
            stack = AsyncExitStack()

            with patch.object(
                OrchestratorLoader,
                "from_settings",
                new=AsyncMock(return_value="orch"),
            ) as lfs_patch:
                loader = OrchestratorLoader(
                    hub=hub,
                    logger=logger,
                    participant_id=uuid4(),
                    stack=stack,
                )
                result = await loader.from_file(
                    path,
                    agent_id=uuid4(),
                )

                self.assertEqual(result, "orch")
                lfs_patch.assert_awaited_once()
                tool_settings = lfs_patch.call_args.kwargs["tool_settings"]
                browser_settings = tool_settings.browser
                self.assertTrue(browser_settings.debug)
                self.assertIsNotNone(browser_settings.debug_source)
                self.assertEqual(browser_settings.debug_source.read(), "debug")
                browser_settings.debug_source.close()
                dbs = tool_settings.database
                self.assertIsNone(dbs)
            await stack.aclose()

    async def test_json_settings_provided(self):
        config = """
[agent]
role = \"assistant\"
task = \"do\"
goal_instructions = \"ins\"

[engine]
uri = \"ai://local/model\"

[json]
value = { type = \"string\", description = \"d\" }
"""
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            hub = MagicMock(spec=HuggingfaceHub)
            logger = MagicMock(spec=Logger)
            stack = AsyncExitStack()

            with patch.object(
                OrchestratorLoader,
                "from_settings",
                new=AsyncMock(return_value="orch"),
            ) as lfs_patch:
                loader = OrchestratorLoader(
                    hub=hub,
                    logger=logger,
                    participant_id=uuid4(),
                    stack=stack,
                )
                result = await loader.from_file(
                    path,
                    agent_id=uuid4(),
                )

                self.assertEqual(result, "orch")
                lfs_patch.assert_awaited_once()
                settings = lfs_patch.call_args.args[0]
                self.assertEqual(
                    settings.json_config,
                    {"value": {"type": "string", "description": "d"}},
                )
        await stack.aclose()

    async def test_file_delivery_profile_hint_is_not_engine_setting(self):
        config = """
[agent]
role = \"assistant\"

[engine]
uri = \"ai://local/model\"
file_delivery_profile = \"multimodal\"
"""
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            hub = MagicMock(spec=HuggingfaceHub)
            logger = MagicMock(spec=Logger)
            stack = AsyncExitStack()

            with patch.object(
                OrchestratorLoader,
                "from_settings",
                new=AsyncMock(return_value="orch"),
            ) as lfs_patch:
                loader = OrchestratorLoader(
                    hub=hub,
                    logger=logger,
                    participant_id=uuid4(),
                    stack=stack,
                )
                result = await loader.from_file(path, agent_id=uuid4())

                self.assertEqual(result, "orch")
                settings = lfs_patch.call_args.args[0]
                self.assertEqual(settings.engine_config, {})
            await stack.aclose()

    async def test_invalid_file_delivery_profile_hint_rejects(self):
        for raw_value in ('"binary"', "42"):
            with self.subTest(raw_value=raw_value):
                config = f"""
[agent]
role = \"assistant\"

[engine]
uri = \"ai://local/model\"
file_delivery_profile = {raw_value}
"""
                with TemporaryDirectory() as tmp:
                    path = f"{tmp}/agent.toml"
                    with open(path, "w", encoding="utf-8") as fh:
                        fh.write(config)

                    hub = MagicMock(spec=HuggingfaceHub)
                    logger = MagicMock(spec=Logger)
                    stack = AsyncExitStack()
                    loader = OrchestratorLoader(
                        hub=hub,
                        logger=logger,
                        participant_id=uuid4(),
                        stack=stack,
                    )

                    with self.assertRaises(AssertionError):
                        await loader.from_file(path, agent_id=uuid4())
                    with self.assertRaises(AssertionError):
                        run_async(OrchestratorLoader.validate_agent_file(path))
                    await stack.aclose()

    async def test_hosted_file_delivery_profile_hint_rejects(self):
        config = """
[agent]
role = \"assistant\"

[engine]
uri = \"ai://openai/deployment\"
file_delivery_profile = \"multimodal\"
"""
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            hub = MagicMock(spec=HuggingfaceHub)
            logger = MagicMock(spec=Logger)
            stack = AsyncExitStack()
            loader = OrchestratorLoader(
                hub=hub,
                logger=logger,
                participant_id=uuid4(),
                stack=stack,
            )

            with self.assertRaisesRegex(AssertionError, "local models"):
                await loader.from_file(path, agent_id=uuid4())
            with self.assertRaisesRegex(AssertionError, "local models"):
                run_async(OrchestratorLoader.validate_agent_file(path))
            await stack.aclose()

    async def test_run_chat_settings_from_file(self):
        config = """
[agent]
role = \"assistant\"

[engine]
uri = \"ai://local/model\"

[run.chat]
enable_thinking = true
"""
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            hub = MagicMock(spec=HuggingfaceHub)
            logger = MagicMock(spec=Logger)
            stack = AsyncExitStack()

            with patch.object(
                OrchestratorLoader,
                "from_settings",
                new=AsyncMock(return_value="orch"),
            ) as lfs_patch:
                loader = OrchestratorLoader(
                    hub=hub,
                    logger=logger,
                    participant_id=uuid4(),
                    stack=stack,
                )
                result = await loader.from_file(
                    path,
                    agent_id=uuid4(),
                )

                self.assertEqual(result, "orch")
                lfs_patch.assert_awaited_once()
                settings = lfs_patch.call_args.args[0]
                self.assertTrue(
                    settings.call_options["chat_settings"]["enable_thinking"]
                )
            await stack.aclose()

    async def test_run_reasoning_settings_from_file(self):
        config = """
[agent]
role = \"assistant\"

[engine]
uri = \"ai://local/model\"

[run.reasoning]
effort = \"xhigh\"
"""
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            hub = MagicMock(spec=HuggingfaceHub)
            logger = MagicMock(spec=Logger)
            stack = AsyncExitStack()

            with patch.object(
                OrchestratorLoader,
                "from_settings",
                new=AsyncMock(return_value="orch"),
            ) as lfs_patch:
                loader = OrchestratorLoader(
                    hub=hub,
                    logger=logger,
                    participant_id=uuid4(),
                    stack=stack,
                )
                result = await loader.from_file(
                    path,
                    agent_id=uuid4(),
                )

                self.assertEqual(result, "orch")
                lfs_patch.assert_awaited_once()
                settings = lfs_patch.call_args.args[0]
                self.assertEqual(
                    settings.call_options["reasoning"]["effort"], "xhigh"
                )
            await stack.aclose()

    async def test_permanent_memory_from_file(self):
        config_tmpl = """
[agent]
role = \"assistant\"
task = \"do\"
goal_instructions = \"how\"

[engine]
uri = \"ai://local/model\"

[memory]
permanent = {{ {entries} }}
"""
        cases = [
            {"code": ("dsn1", None)},
            {"code": ("dsn1", None), "docs": ("dsn2", None)},
            {
                "code": ("dsn1", "Code entries"),
                "docs": ("dsn2", None),
                "more": ("dsn3", "More docs"),
            },
        ]

        for case in cases:
            with self.subTest(case=case):
                entries = ", ".join(
                    f'{k} = "{dsn}{"," + desc if desc else ""}"'
                    for k, (dsn, desc) in case.items()
                )
                config = config_tmpl.format(entries=entries)
                with TemporaryDirectory() as tmp:
                    path = f"{tmp}/agent.toml"
                    with open(path, "w", encoding="utf-8") as fh:
                        fh.write(config)

                    stack = AsyncExitStack()
                    with patch.object(
                        OrchestratorLoader,
                        "from_settings",
                        new=AsyncMock(return_value="orch"),
                    ) as lfs_patch:
                        loader = OrchestratorLoader(
                            hub=MagicMock(spec=HuggingfaceHub),
                            logger=MagicMock(spec=Logger),
                            participant_id=uuid4(),
                            stack=stack,
                        )
                        result = await loader.from_file(
                            path,
                            agent_id=uuid4(),
                        )

                        self.assertEqual(result, "orch")
                        lfs_patch.assert_awaited_once()
                        settings = lfs_patch.call_args.args[0]
                        expected = {
                            k: PermanentMemoryStoreSettings(
                                dsn=dsn, description=desc
                            )
                            for k, (dsn, desc) in case.items()
                        }
                        self.assertEqual(settings.permanent_memory, expected)
                    await stack.aclose()

    async def test_engine_only_generates_id(self):
        config = """
[agent]

[engine]
uri = \"ai://local/model\"
"""
        uid = uuid4()
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            with (
                patch.object(
                    OrchestratorLoader,
                    "from_settings",
                    new=AsyncMock(return_value="orch"),
                ) as lfs_patch,
                patch("avalan.agent.loader.uuid4", return_value=uid),
            ):
                stack = AsyncExitStack()
                loader = OrchestratorLoader(
                    hub=MagicMock(spec=HuggingfaceHub),
                    logger=MagicMock(spec=Logger),
                    participant_id=uuid4(),
                    stack=stack,
                )
                result = await loader.from_file(str(path), agent_id=None)

                self.assertEqual(result, "orch")
                lfs_patch.assert_awaited_once()
                settings = lfs_patch.call_args.args[0]
                self.assertEqual(settings.agent_id, uid)
                self.assertEqual(settings.uri, "ai://local/model")
            await stack.aclose()

    async def test_engine_with_id(self):
        uid = uuid4()
        config = f"""
[agent]
id = \"{uid}\"

[engine]
uri = \"ai://local/model\"
"""
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            with patch.object(
                OrchestratorLoader,
                "from_settings",
                new=AsyncMock(return_value="orch"),
            ) as lfs_patch:
                stack = AsyncExitStack()
                loader = OrchestratorLoader(
                    hub=MagicMock(spec=HuggingfaceHub),
                    logger=MagicMock(spec=Logger),
                    participant_id=uuid4(),
                    stack=stack,
                )
                result = await loader.from_file(path, agent_id=None)

                self.assertEqual(result, "orch")
                lfs_patch.assert_awaited_once()
                settings = lfs_patch.call_args.args[0]
                self.assertEqual(settings.agent_id, str(uid))
            await stack.aclose()

    async def test_engine_with_name(self):
        config = """
[agent]
name = \"Agent\"

[engine]
uri = \"ai://local/model\"
"""
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            with patch.object(
                OrchestratorLoader,
                "from_settings",
                new=AsyncMock(return_value="orch"),
            ) as lfs_patch:
                stack = AsyncExitStack()
                loader = OrchestratorLoader(
                    hub=MagicMock(spec=HuggingfaceHub),
                    logger=MagicMock(spec=Logger),
                    participant_id=uuid4(),
                    stack=stack,
                )
                result = await loader.from_file(path, agent_id=None)

                self.assertEqual(result, "orch")
                lfs_patch.assert_awaited_once()
                settings = lfs_patch.call_args.args[0]
                self.assertEqual(settings.agent_config.get("name"), "Agent")
            await stack.aclose()

    async def test_engine_generation_settings(self):
        config = """
[agent]

[engine]
uri = \"ai://local/model\"

[run]
temperature = 0.5
top_p = 0.9
top_k = 5
max_new_tokens = 42
"""
        with TemporaryDirectory() as tmp:
            path = f"{tmp}/agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            with patch.object(
                OrchestratorLoader,
                "from_settings",
                new=AsyncMock(return_value="orch"),
            ) as lfs_patch:
                stack = AsyncExitStack()
                loader = OrchestratorLoader(
                    hub=MagicMock(spec=HuggingfaceHub),
                    logger=MagicMock(spec=Logger),
                    participant_id=uuid4(),
                    stack=stack,
                )
                result = await loader.from_file(path, agent_id=None)

                self.assertEqual(result, "orch")
                lfs_patch.assert_awaited_once()
                settings = lfs_patch.call_args.args[0]
                self.assertEqual(settings.call_options["temperature"], 0.5)
                self.assertEqual(settings.call_options["top_p"], 0.9)
                self.assertEqual(settings.call_options["top_k"], 5)
                self.assertEqual(settings.call_options["max_new_tokens"], 42)
            await stack.aclose()

    async def test_run_response_format_schema_ref_is_resolved(self):
        config = """
[agent]

[engine]
uri = \"ai://local/model\"

[run.response_format]
type = \"json_schema\"
name = \"answer\"
schema_ref = \"schemas/answer.json\"
strict = true
"""
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            schema_dir = root / "schemas"
            path = root / "agent.toml"
            schema_dir.mkdir()
            with open(schema_dir / "answer.json", "w", encoding="utf-8") as fh:
                fh.write('{"type": "object"}')
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            with patch.object(
                OrchestratorLoader,
                "from_settings",
                new=AsyncMock(return_value="orch"),
            ) as lfs_patch:
                stack = AsyncExitStack()
                loader = OrchestratorLoader(
                    hub=MagicMock(spec=HuggingfaceHub),
                    logger=MagicMock(spec=Logger),
                    participant_id=uuid4(),
                    stack=stack,
                )
                result = await loader.from_file(path, agent_id=None)

                self.assertEqual(result, "orch")
                settings = lfs_patch.call_args.args[0]
                response_format = settings.call_options["response_format"]
                self.assertEqual(
                    response_format["schema"],
                    {"type": "object"},
                )
                self.assertNotIn("schema_ref", response_format)
            await stack.aclose()

    async def test_chat_style_response_format_schema_ref_is_resolved(self):
        config = """
[agent]

[engine]
uri = \"ai://local/model\"

[run.response_format]
type = \"json_schema\"

[run.response_format.json_schema]
name = \"answer\"
schema_ref = \"schemas/answer.json\"
"""
        with TemporaryDirectory() as tmp:
            root = Path(tmp)
            schema_dir = root / "schemas"
            path = root / "agent.toml"
            schema_dir.mkdir()
            with open(schema_dir / "answer.json", "w", encoding="utf-8") as fh:
                fh.write('{"type": "object"}')
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)

            with patch.object(
                OrchestratorLoader,
                "from_settings",
                new=AsyncMock(return_value="orch"),
            ) as lfs_patch:
                stack = AsyncExitStack()
                loader = OrchestratorLoader(
                    hub=MagicMock(spec=HuggingfaceHub),
                    logger=MagicMock(spec=Logger),
                    participant_id=uuid4(),
                    stack=stack,
                )
                await loader.from_file(str(path), agent_id=None)

                settings = lfs_patch.call_args.args[0]
                json_schema = settings.call_options["response_format"][
                    "json_schema"
                ]
                self.assertEqual(json_schema["schema"], {"type": "object"})
                self.assertNotIn("schema_ref", json_schema)
            await stack.aclose()

    async def test_response_format_schema_ref_rejects_escape_safely(self):
        config = """
[agent]

[engine]
uri = \"ai://local/model\"

[run.response_format]
type = \"json_schema\"
name = \"answer\"
schema_ref = \"../private/answer.json\"
"""
        with TemporaryDirectory() as tmp:
            path = Path(tmp) / "agent.toml"
            with open(path, "w", encoding="utf-8") as fh:
                fh.write(config)
            stack = AsyncExitStack()
            loader = OrchestratorLoader(
                hub=MagicMock(spec=HuggingfaceHub),
                logger=MagicMock(spec=Logger),
                participant_id=uuid4(),
                stack=stack,
            )

            with self.assertRaises(AssertionError) as error:
                await loader.from_file(str(path), agent_id=None)

            self.assertIn(
                "run.response_format.schema_ref",
                str(error.exception),
            )
            self.assertNotIn("private/answer", str(error.exception))
            await stack.aclose()


class LoaderFromSettingsTestCase(IsolatedAsyncioTestCase):
    async def test_load_default_orchestrator_from_settings(self):
        hub = MagicMock(spec=HuggingfaceHub)
        logger = MagicMock(spec=Logger)
        stack = AsyncExitStack()

        sentence_model = MagicMock()
        sentence_model.__enter__.return_value = sentence_model

        model_manager = MagicMock()
        model_manager.__enter__.return_value = model_manager
        model_manager.parse_uri.return_value = "uri_obj"
        model_manager.get_engine_settings.return_value = "settings_obj"

        memory = MagicMock()
        tool = MagicMock()
        event_manager = MagicMock()

        settings = OrchestratorSettings(
            agent_id=uuid4(),
            orchestrator_type=None,
            agent_config={"role": "assistant"},
            uri="ai://local/model",
            engine_config={},
            tools=None,
            call_options=None,
            template_vars=None,
            memory_permanent_message=None,
            permanent_memory=None,
            memory_recent=False,
            sentence_model_id=OrchestratorLoader.DEFAULT_SENTENCE_MODEL_ID,
            sentence_model_engine_config=None,
            sentence_model_max_tokens=500,
            sentence_model_overlap_size=125,
            sentence_model_window_size=250,
            json_config=None,
            log_events=True,
        )

        with (
            patch(
                "avalan.agent.loader.SentenceTransformerModel",
                return_value=sentence_model,
            ),
            patch("avalan.agent.loader.TextPartitioner"),
            patch(
                "avalan.agent.loader.MemoryManager.create_instance",
                new=AsyncMock(return_value=memory),
            ) as mm_patch,
            patch(
                "avalan.agent.loader.ModelManager", return_value=model_manager
            ) as model_patch,
            patch(
                "avalan.agent.loader.DefaultOrchestrator", return_value="orch"
            ) as orch_patch,
            patch(
                "avalan.agent.loader.ToolManager.create_instance",
                return_value=tool,
            ),
            patch(
                "avalan.agent.loader.EventManager", return_value=event_manager
            ),
        ):
            loader = OrchestratorLoader(
                hub=hub,
                logger=logger,
                participant_id=uuid4(),
                stack=stack,
            )
            result = await loader.from_settings(settings)

            self.assertEqual(result, "orch")
            orch_patch.assert_called_once()
            model_patch.assert_called_once_with(
                hub, logger, event_manager=event_manager
            )
            mm_patch.assert_awaited_once()
        await stack.aclose()

    async def test_permanent_memory_from_settings(self):
        base_settings = dict(
            agent_id=uuid4(),
            orchestrator_type=None,
            agent_config={"role": "assistant"},
            uri="ai://local/model",
            engine_config={},
            tools=None,
            call_options=None,
            template_vars=None,
            memory_permanent_message=None,
            memory_recent=False,
            sentence_model_id=OrchestratorLoader.DEFAULT_SENTENCE_MODEL_ID,
            sentence_model_engine_config=None,
            sentence_model_max_tokens=500,
            sentence_model_overlap_size=125,
            sentence_model_window_size=250,
            json_config=None,
            log_events=True,
        )

        cases = [
            {
                "code": PermanentMemoryStoreSettings(
                    dsn="dsn1", description=None
                )
            },
            {
                "code": PermanentMemoryStoreSettings(
                    dsn="dsn1", description="Code store"
                ),
                "docs": PermanentMemoryStoreSettings(
                    dsn="dsn2", description=None
                ),
            },
            {
                "code": PermanentMemoryStoreSettings(
                    dsn="dsn1", description="Code entries"
                ),
                "docs": PermanentMemoryStoreSettings(
                    dsn="dsn2", description="Docs store"
                ),
                "more": PermanentMemoryStoreSettings(
                    dsn="dsn3", description=None
                ),
            },
        ]

        for case in cases:
            with self.subTest(case=case):
                hub = MagicMock(spec=HuggingfaceHub)
                logger = MagicMock(spec=Logger)
                stack = AsyncExitStack()

                sentence_model = MagicMock()
                sentence_model.__enter__.return_value = sentence_model

                model_manager = MagicMock()
                model_manager.__enter__.return_value = model_manager
                model_manager.parse_uri.return_value = "uri_obj"
                model_manager.get_engine_settings.return_value = "settings_obj"

                memory = MagicMock()
                tool = MagicMock()
                event_manager = MagicMock()

                settings = OrchestratorSettings(
                    **base_settings,
                    permanent_memory=case,
                )

                store_instances = [MagicMock() for _ in case]
                pgsql_raw_memory = MagicMock()
                pgsql_raw_memory.create_instance = AsyncMock(
                    side_effect=store_instances
                )
                with (
                    patch(
                        "avalan.agent.loader.SentenceTransformerModel",
                        return_value=sentence_model,
                    ),
                    patch("avalan.agent.loader.TextPartitioner"),
                    patch(
                        "avalan.agent.loader.MemoryManager.create_instance",
                        new=AsyncMock(return_value=memory),
                    ),
                    patch(
                        "avalan.agent.loader.ModelManager",
                        return_value=model_manager,
                    ),
                    patch(
                        "avalan.agent.loader.PgsqlRawMemory",
                        pgsql_raw_memory,
                    ),
                    patch(
                        "avalan.agent.loader.DefaultOrchestrator",
                        return_value="orch",
                    ),
                    patch(
                        "avalan.agent.loader.ToolManager.create_instance",
                        return_value=tool,
                    ),
                    patch(
                        "avalan.agent.loader.EventManager",
                        return_value=event_manager,
                    ),
                ):
                    loader = OrchestratorLoader(
                        hub=hub,
                        logger=logger,
                        participant_id=uuid4(),
                        stack=stack,
                    )
                    await loader.from_settings(settings)

                    create_instance = pgsql_raw_memory.create_instance
                    self.assertEqual(create_instance.await_count, len(case))
                    expected_store_calls = [
                        call(dsn=store_settings.dsn, logger=logger)
                        for store_settings in case.values()
                    ]
                    create_instance.assert_has_awaits(expected_store_calls)
                    expected_add_calls = [
                        call(
                            namespace,
                            instance,
                            description=case[namespace].description,
                        )
                        for (namespace, instance) in zip(
                            case.keys(), store_instances
                        )
                    ]
                    self.assertEqual(
                        memory.add_permanent_memory.call_args_list,
                        expected_add_calls,
                    )
                await stack.aclose()

    async def test_database_tool_from_settings(self):
        hub = MagicMock(spec=HuggingfaceHub)
        logger = MagicMock(spec=Logger)
        stack = AsyncExitStack()

        sentence_model = MagicMock()
        sentence_model.__enter__.return_value = sentence_model

        model_manager = MagicMock()
        model_manager.__enter__.return_value = model_manager
        model_manager.parse_uri.return_value = "uri_obj"
        model_manager.get_engine_settings.return_value = "settings_obj"

        memory = MagicMock()
        tool = MagicMock()
        tool.__aenter__.return_value = tool
        event_manager = MagicMock()

        settings = OrchestratorSettings(
            agent_id=uuid4(),
            orchestrator_type=None,
            agent_config={"role": "assistant"},
            uri="ai://local/model",
            engine_config={},
            tools=None,
            call_options=None,
            template_vars=None,
            memory_permanent_message=None,
            permanent_memory=None,
            memory_recent=False,
            sentence_model_id=OrchestratorLoader.DEFAULT_SENTENCE_MODEL_ID,
            sentence_model_engine_config=None,
            sentence_model_max_tokens=500,
            sentence_model_overlap_size=125,
            sentence_model_window_size=250,
            json_config=None,
            log_events=True,
        )

        db_settings = DatabaseToolSettings(dsn="sqlite:///db.sqlite")

        browser_tool = MagicMock()
        code_tool = MagicMock()
        graph_tool = MagicMock()
        math_tool = MagicMock()
        memory_tool = MagicMock()
        db_tool = MagicMock()

        with (
            patch(
                "avalan.agent.loader.SentenceTransformerModel",
                return_value=sentence_model,
            ),
            patch("avalan.agent.loader.TextPartitioner"),
            patch(
                "avalan.agent.loader.MemoryManager.create_instance",
                new=AsyncMock(return_value=memory),
            ),
            patch(
                "avalan.agent.loader.ModelManager", return_value=model_manager
            ),
            patch(
                "avalan.agent.loader.DefaultOrchestrator", return_value="orch"
            ),
            patch(
                "avalan.agent.loader.ToolManager.create_instance",
                return_value=tool,
            ) as tm_patch,
            patch(
                "avalan.agent.loader.EventManager", return_value=event_manager
            ),
            patch(
                "avalan.agent.loader.BrowserToolSet", return_value=browser_tool
            ),
            patch("avalan.agent.loader.CodeToolSet", return_value=code_tool),
            patch(
                "avalan.agent.loader.GraphToolSet", return_value=graph_tool
            ) as graph_patch,
            patch("avalan.agent.loader.HAS_GRAPH_DEPENDENCIES", True),
            patch("avalan.agent.loader.MathToolSet", return_value=math_tool),
            patch(
                "avalan.agent.loader.MemoryToolSet", return_value=memory_tool
            ),
            patch(
                "avalan.agent.loader.DatabaseToolSet",
                return_value=db_tool,
            ) as db_patch,
        ):
            loader = OrchestratorLoader(
                hub=hub,
                logger=logger,
                participant_id=uuid4(),
                stack=stack,
            )
            result = await loader.from_settings(
                settings,
                tool_settings=ToolSettingsContext(database=db_settings),
            )

            self.assertEqual(result, "orch")
            db_patch.assert_called_once_with(
                settings=db_settings, namespace="database"
            )
            available = tm_patch.call_args.kwargs["available_toolsets"]
            graph_patch.assert_called_once_with(
                settings=GraphToolSettings(), namespace="graph"
            )
            self.assertIn(graph_tool, available)
            self.assertIn(db_tool, available)
        await stack.aclose()

    async def test_tool_settings_logged(self):
        hub = MagicMock(spec=HuggingfaceHub)
        logger = MagicMock(spec=Logger)
        stack = AsyncExitStack()

        sentence_model = MagicMock()
        sentence_model.__enter__.return_value = sentence_model

        model_manager = MagicMock()
        model_manager.__enter__.return_value = model_manager
        model_manager.parse_uri.return_value = "uri_obj"
        model_manager.get_engine_settings.return_value = "settings_obj"

        memory = MagicMock()
        tool = MagicMock()
        tool.__aenter__ = AsyncMock(return_value=tool)
        event_manager = MagicMock()

        settings = OrchestratorSettings(
            agent_id=uuid4(),
            orchestrator_type=None,
            agent_config={"role": "assistant"},
            uri="ai://local/model",
            engine_config={},
            tools=None,
            call_options=None,
            template_vars=None,
            memory_permanent_message=None,
            permanent_memory=None,
            memory_recent=False,
            sentence_model_id=OrchestratorLoader.DEFAULT_SENTENCE_MODEL_ID,
            sentence_model_engine_config=None,
            sentence_model_max_tokens=500,
            sentence_model_overlap_size=125,
            sentence_model_window_size=250,
            json_config=None,
            log_events=True,
        )

        browser_settings = BrowserToolSettings()
        db_settings = DatabaseToolSettings(dsn="sqlite:///db.sqlite")
        graph_settings = GraphToolSettings(file="/tmp/chart.png")

        with (
            patch(
                "avalan.agent.loader.SentenceTransformerModel",
                return_value=sentence_model,
            ),
            patch("avalan.agent.loader.TextPartitioner"),
            patch(
                "avalan.agent.loader.MemoryManager.create_instance",
                new=AsyncMock(return_value=memory),
            ),
            patch(
                "avalan.agent.loader.ModelManager",
                return_value=model_manager,
            ),
            patch(
                "avalan.agent.loader.DefaultOrchestrator",
                return_value="orch",
            ),
            patch(
                "avalan.agent.loader.ToolManager.create_instance",
                return_value=tool,
            ),
            patch(
                "avalan.agent.loader.EventManager", return_value=event_manager
            ),
            patch("avalan.agent.loader.BrowserToolSet"),
            patch("avalan.agent.loader.CodeToolSet"),
            patch("avalan.agent.loader.MathToolSet"),
            patch("avalan.agent.loader.MemoryToolSet"),
            patch("avalan.agent.loader.DatabaseToolSet"),
        ):
            loader = OrchestratorLoader(
                hub=hub,
                logger=logger,
                participant_id=uuid4(),
                stack=stack,
            )
            result = await loader.from_settings(
                settings,
                tool_settings=ToolSettingsContext(
                    browser=browser_settings,
                    database=db_settings,
                    graph=graph_settings,
                ),
            )

            self.assertEqual(result, "orch")
            logger.log.assert_any_call(
                DEBUG,
                "<OrchestratorLoader> Tool settings: browser=%s, database=%s,"
                " graph=%s",
                browser_settings,
                db_settings,
                graph_settings,
            )
        await stack.aclose()

    async def test_event_logging_listener(self):
        hub = MagicMock(spec=HuggingfaceHub)
        logger = MagicMock(spec=Logger)
        stack = AsyncExitStack()

        sentence_model = MagicMock()
        sentence_model.__enter__.return_value = sentence_model

        model_manager = MagicMock()
        model_manager.__enter__.return_value = model_manager
        model_manager.parse_uri.return_value = "uri_obj"
        model_manager.get_engine_settings.return_value = "settings_obj"

        memory = MagicMock()
        tool = MagicMock()
        tool.__aenter__ = AsyncMock(return_value=tool)
        tool.__aexit__ = AsyncMock(return_value=None)

        class DummyEventManager:
            def __init__(self) -> None:
                self.listeners: list[Callable[[Event], Any]] = []

            def add_listener(
                self,
                listener: Callable[[Event], Any],
                event_types: Any | None = None,
            ) -> None:
                self.listeners.append(listener)

        event_manager = DummyEventManager()

        settings = OrchestratorSettings(
            agent_id=uuid4(),
            orchestrator_type=None,
            agent_config={"role": "assistant"},
            uri="ai://local/model",
            engine_config={},
            tools=None,
            call_options=None,
            template_vars=None,
            memory_permanent_message=None,
            permanent_memory=None,
            memory_recent=False,
            sentence_model_id=OrchestratorLoader.DEFAULT_SENTENCE_MODEL_ID,
            sentence_model_engine_config=None,
            sentence_model_max_tokens=500,
            sentence_model_overlap_size=125,
            sentence_model_window_size=250,
            json_config=None,
            log_events=True,
        )

        with (
            patch(
                "avalan.agent.loader.SentenceTransformerModel",
                return_value=sentence_model,
            ),
            patch("avalan.agent.loader.TextPartitioner"),
            patch(
                "avalan.agent.loader.MemoryManager.create_instance",
                new=AsyncMock(return_value=memory),
            ),
            patch(
                "avalan.agent.loader.ModelManager", return_value=model_manager
            ),
            patch(
                "avalan.agent.loader.DefaultOrchestrator", return_value="orch"
            ),
            patch(
                "avalan.agent.loader.ToolManager.create_instance",
                return_value=tool,
            ),
            patch(
                "avalan.agent.loader.EventManager", return_value=event_manager
            ),
        ):
            loader = OrchestratorLoader(
                hub=hub,
                logger=logger,
                participant_id=uuid4(),
                stack=stack,
            )
            result = await loader.from_settings(settings)

        self.assertEqual(result, "orch")
        self.assertEqual(len(event_manager.listeners), 1)

        listener = event_manager.listeners[0]
        logger.log.reset_mock()

        tool_event = Event(
            type=EventType.TOOL_PROCESS,
            payload={"message": "tool running"},
        )
        listener(tool_event)
        logger.log.assert_called_once()
        level, message, payload = logger.log.call_args.args
        self.assertEqual(level, INFO)
        self.assertEqual(
            message, "<Event tool_process @ OrchestratorLoader> %s"
        )
        self.assertEqual(payload, tool_event.payload)

        logger.log.reset_mock()

        debug_event = Event(
            type=EventType.START,
            payload={"message": "start"},
        )
        listener(debug_event)
        logger.log.assert_called_once()
        level, message, payload = logger.log.call_args.args
        self.assertEqual(level, DEBUG)
        self.assertEqual(message, "<Event start @ OrchestratorLoader> %s")
        self.assertEqual(payload, debug_event.payload)

        await stack.aclose()

    async def test_load_json_orchestrator_from_settings(self):
        hub = MagicMock(spec=HuggingfaceHub)
        logger = MagicMock(spec=Logger)
        stack = AsyncExitStack()

        sentence_model = MagicMock()
        sentence_model.__enter__.return_value = sentence_model

        model_manager = MagicMock()
        model_manager.__enter__.return_value = model_manager
        model_manager.parse_uri.return_value = "uri_obj"
        model_manager.get_engine_settings.return_value = "settings_obj"

        memory = MagicMock()
        tool = MagicMock()
        event_manager = MagicMock()

        settings = OrchestratorSettings(
            agent_id=uuid4(),
            orchestrator_type="json",
            agent_config={
                "role": "assistant",
                "task": "do",
                "goal_instructions": "how",
            },
            uri="ai://local/model",
            engine_config={},
            tools=None,
            call_options=None,
            template_vars=None,
            memory_permanent_message=None,
            permanent_memory=None,
            memory_recent=False,
            sentence_model_id=OrchestratorLoader.DEFAULT_SENTENCE_MODEL_ID,
            sentence_model_engine_config=None,
            sentence_model_max_tokens=500,
            sentence_model_overlap_size=125,
            sentence_model_window_size=250,
            json_config={"value": {"type": "string", "description": "d"}},
            log_events=True,
        )

        with (
            patch(
                "avalan.agent.loader.SentenceTransformerModel",
                return_value=sentence_model,
            ),
            patch("avalan.agent.loader.TextPartitioner"),
            patch(
                "avalan.agent.loader.MemoryManager.create_instance",
                new=AsyncMock(return_value=memory),
            ),
            patch(
                "avalan.agent.loader.ModelManager", return_value=model_manager
            ),
            patch.object(
                OrchestratorLoader,
                "_load_json_orchestrator",
                return_value="json_orch",
            ) as json_patch,
            patch(
                "avalan.agent.loader.ToolManager.create_instance",
                return_value=tool,
            ),
            patch(
                "avalan.agent.loader.EventManager", return_value=event_manager
            ),
        ):
            loader = OrchestratorLoader(
                hub=hub,
                logger=logger,
                participant_id=uuid4(),
                stack=stack,
            )
            result = await loader.from_settings(settings)

            self.assertEqual(result, "json_orch")
            json_patch.assert_called_once()
        await stack.aclose()

    async def test_load_json_orchestrator_properties(self):
        agent_id = uuid4()
        engine_uri = MagicMock()
        engine_settings = MagicMock()
        logger = MagicMock()
        model_manager = MagicMock()
        memory = MagicMock()
        tool = MagicMock()
        event_manager = MagicMock()

        config = {
            "json": {
                "name": {"type": "string", "description": "n"},
                "age": {"type": "integer", "description": "a"},
            }
        }

        agent_config = {
            "role": "assistant",
            "task": "do",
            "goal_instructions": "how",
        }

        with patch("avalan.agent.loader.JsonOrchestrator") as orch_patch:
            OrchestratorLoader._load_json_orchestrator(
                agent_id=agent_id,
                engine_uri=engine_uri,
                engine_settings=engine_settings,
                logger=logger,
                model_manager=model_manager,
                memory=memory,
                tool=tool,
                event_manager=event_manager,
                config=config,
                agent_config=agent_config,
                call_options=None,
                template_vars=None,
            )

            orch_patch.assert_called_once()
            properties = orch_patch.call_args.args[6]
            self.assertEqual(len(properties), 2)
            self.assertEqual(properties[0].name, "name")
            self.assertEqual(properties[1].data_type, "integer")


if __name__ == "__main__":
    main()
