"""
Copyright (C) 2026 Garudex Labs.  All Rights Reserved.
Caracal, a product of Garudex Labs

SDK primitives unit tests: spawn and delegate context manager flows.
"""

import unittest

import httpx

from caracalai_sdk.coordinator import AgentKind, CoordinatorClient
from caracalai_sdk.context import current
from caracalai_sdk.primitives import spawn, delegate, delegate_to_spawn


def _coord(handler) -> CoordinatorClient:
    return CoordinatorClient(
        base_url="http://coord.test",
        _client=httpx.AsyncClient(transport=httpx.MockTransport(handler)),
    )


def _default_handler(req: httpx.Request) -> httpx.Response:
    if req.method == "POST" and str(req.url).endswith("/agents"):
        return httpx.Response(200, json={"agent_session_id": "agent-1"})
    if req.method == "DELETE":
        return httpx.Response(204)
    if req.method == "POST" and str(req.url).endswith("/delegations"):
        return httpx.Response(200, json={"delegation_edge_id": "edge-1"})
    return httpx.Response(404)


class SpawnTests(unittest.IsolatedAsyncioTestCase):
    async def test_yields_context_with_agent_session_id(self) -> None:
        coord = _coord(_default_handler)
        async with spawn(
            coordinator=coord,
            zone_id="z",
            application_id="app",
            subject_token="tok",
        ) as ctx:
            self.assertEqual(ctx.agent_session_id, "agent-1")
            self.assertEqual(ctx.zone_id, "z")
            self.assertIsNotNone(current())

    async def test_sets_ambient_context_and_clears_on_exit(self) -> None:
        coord = _coord(_default_handler)
        async with spawn(
            coordinator=coord, zone_id="z", application_id="app", subject_token="tok"
        ):
            self.assertIsNotNone(current())
        self.assertIsNone(current())

    async def test_service_kind_skips_termination(self) -> None:
        requests: list[httpx.Request] = []

        async def handler(req: httpx.Request) -> httpx.Response:
            requests.append(req)
            if req.method == "POST":
                return httpx.Response(200, json={"agent_session_id": "agent-1"})
            return httpx.Response(204)

        coord = _coord(handler)
        async with spawn(
            coordinator=coord, zone_id="z", application_id="app",
            subject_token="tok", kind=AgentKind.SERVICE,
        ):
            pass

        methods = [r.method for r in requests]
        self.assertNotIn("DELETE", methods)

    async def test_non_service_kind_terminates_on_exit(self) -> None:
        requests: list[httpx.Request] = []

        async def handler(req: httpx.Request) -> httpx.Response:
            requests.append(req)
            if req.method == "POST":
                return httpx.Response(200, json={"agent_session_id": "agent-1"})
            return httpx.Response(204)

        coord = _coord(handler)
        async with spawn(
            coordinator=coord, zone_id="z", application_id="app",
            subject_token="tok", kind=AgentKind.EPHEMERAL,
        ):
            pass

        methods = [r.method for r in requests]
        self.assertIn("DELETE", methods)

    async def test_on_agent_start_hook_called(self) -> None:
        events: list[str] = []

        async def on_start(ctx) -> None:
            events.append(f"start:{ctx.agent_session_id}")

        coord = _coord(_default_handler)
        async with spawn(
            coordinator=coord, zone_id="z", application_id="app",
            subject_token="tok", on_agent_start=on_start,
        ):
            pass

        self.assertEqual(events, ["start:agent-1"])

    async def test_on_agent_end_hook_called(self) -> None:
        events: list[str] = []

        async def on_end(ctx) -> None:
            events.append(f"end:{ctx.agent_session_id}")

        coord = _coord(_default_handler)
        async with spawn(
            coordinator=coord, zone_id="z", application_id="app",
            subject_token="tok", on_agent_end=on_end,
        ):
            pass

        self.assertEqual(events, ["end:agent-1"])

    async def test_propagates_coordinator_error(self) -> None:
        async def handler(req: httpx.Request) -> httpx.Response:
            return httpx.Response(500)

        coord = _coord(handler)
        with self.assertRaises(httpx.HTTPStatusError):
            async with spawn(
                coordinator=coord, zone_id="z", application_id="app", subject_token="tok"
            ):
                pass  # pragma: no cover


class DelegateTests(unittest.IsolatedAsyncioTestCase):
    async def test_raises_without_active_agent_session(self) -> None:
        coord = _coord(_default_handler)
        with self.assertRaises(RuntimeError):
            async with delegate(
                coordinator=coord,
                to_agent_session_id="agent-2",
                to_application_id="app-2",
                scopes=["tool:call"],
            ):
                pass  # pragma: no cover

    async def test_yields_child_context_with_delegation_edge(self) -> None:
        coord = _coord(_default_handler)
        async with spawn(
            coordinator=coord, zone_id="z", application_id="app", subject_token="tok"
        ) as parent:
            async with delegate(
                coordinator=coord,
                to_agent_session_id="agent-2",
                to_application_id="app-2",
                scopes=["tool:call"],
            ) as child:
                self.assertEqual(child.delegation_edge_id, "edge-1")
                self.assertEqual(child.hop, parent.hop + 1)
                self.assertEqual(child.parent_edge_id, parent.delegation_edge_id)

    async def test_restores_parent_context_on_exit(self) -> None:
        coord = _coord(_default_handler)
        async with spawn(
            coordinator=coord, zone_id="z", application_id="app", subject_token="tok"
        ) as parent:
            async with delegate(
                coordinator=coord,
                to_agent_session_id="agent-2",
                to_application_id="app-2",
                scopes=["tool:call"],
            ):
                pass
            self.assertEqual(current().agent_session_id, parent.agent_session_id)


class DelegateToSpawnTests(unittest.IsolatedAsyncioTestCase):
    async def test_raises_without_active_parent(self) -> None:
        coord = _coord(_default_handler)
        with self.assertRaises(RuntimeError):
            async with delegate_to_spawn(
                coordinator=coord, zone_id="z", application_id="app",
                subject_token="tok", scopes=["tool:call"],
            ):
                pass  # pragma: no cover

    async def test_records_spawn_then_delegation_in_order(self) -> None:
        calls: list[tuple[str, str]] = []

        async def handler(req: httpx.Request) -> httpx.Response:
            path = req.url.path
            calls.append((req.method, path))
            if req.method == "POST" and path.endswith("/agents"):
                return httpx.Response(200, json={"agent_session_id": "child-1"})
            if req.method == "POST" and path.endswith("/delegations"):
                return httpx.Response(200, json={"delegation_edge_id": "edge-9"})
            if req.method == "DELETE":
                return httpx.Response(204)
            return httpx.Response(404)

        coord = _coord(handler)
        async with spawn(
            coordinator=coord, zone_id="z", application_id="app", subject_token="tok"
        ) as parent:
            async with delegate_to_spawn(
                coordinator=coord, zone_id="z", application_id="app-child",
                subject_token="tok", scopes=["tool:call"],
            ) as child:
                self.assertEqual(child.agent_session_id, "child-1")
                self.assertEqual(child.delegation_edge_id, "edge-9")
                self.assertEqual(child.parent_edge_id, parent.delegation_edge_id)
                self.assertEqual(child.hop, parent.hop + 1)

        posts = [c for c in calls if c[0] == "POST"]
        self.assertEqual(len(posts), 3)
        self.assertTrue(posts[1][1].endswith("/agents"))
        self.assertTrue(posts[2][1].endswith("/delegations"))
        self.assertTrue(any(m == "DELETE" for m, _ in calls))

    async def test_service_kind_skips_termination(self) -> None:
        calls: list[str] = []

        async def handler(req: httpx.Request) -> httpx.Response:
            calls.append(req.method)
            if req.method == "POST" and req.url.path.endswith("/agents"):
                return httpx.Response(200, json={"agent_session_id": "child-1"})
            if req.method == "POST" and req.url.path.endswith("/delegations"):
                return httpx.Response(200, json={"delegation_edge_id": "edge-9"})
            return httpx.Response(204)

        coord = _coord(handler)
        async with spawn(
            coordinator=coord, zone_id="z", application_id="app", subject_token="tok"
        ):
            async with delegate_to_spawn(
                coordinator=coord, zone_id="z", application_id="app-child",
                subject_token="tok", scopes=["tool:call"], kind=AgentKind.SERVICE,
            ):
                pass

        self.assertEqual(calls.count("DELETE"), 1)


class ParentCtxOverrideTests(unittest.IsolatedAsyncioTestCase):
    """CP-3: spawn / delegate_to_spawn must accept an explicit parent context."""

    async def test_spawn_uses_explicit_parent_ctx_when_no_current(self) -> None:
        from caracalai_sdk.context import CaracalContext

        captured: dict = {}

        def handler(req: httpx.Request) -> httpx.Response:
            if req.method == "POST" and str(req.url).endswith("/agents"):
                import json

                captured["body"] = json.loads(req.content.decode())
                return httpx.Response(200, json={"agent_session_id": "agent-2"})
            return httpx.Response(204)

        parent = CaracalContext(
            subject_token="parent-tok",
            zone_id="z",
            client_id="parent-app",
            agent_session_id="parent-session",
            hop=2,
            trace_id="11111111111111111111111111111111",
        )
        coord = _coord(handler)
        self.assertIsNone(current())
        async with spawn(
            coordinator=coord,
            zone_id="z",
            application_id="child-app",
            subject_token="tok",
            parent_ctx=parent,
        ) as ctx:
            self.assertEqual(ctx.agent_session_id, "agent-2")
            self.assertEqual(ctx.hop, 2)
            self.assertEqual(ctx.trace_id, "11111111111111111111111111111111")
        self.assertEqual(captured["body"].get("parent_id"), "parent-session")

    async def test_delegate_to_spawn_uses_explicit_parent_ctx(self) -> None:
        from caracalai_sdk.context import CaracalContext

        seen = {"delegations": 0, "agents": 0}

        def handler(req: httpx.Request) -> httpx.Response:
            url = str(req.url)
            if req.method == "POST" and url.endswith("/delegations"):
                seen["delegations"] += 1
                return httpx.Response(200, json={"delegation_edge_id": "edge-9"})
            if req.method == "POST" and url.endswith("/agents"):
                seen["agents"] += 1
                return httpx.Response(200, json={"agent_session_id": "agent-9"})
            return httpx.Response(204)

        parent = CaracalContext(
            subject_token="parent-tok",
            zone_id="z",
            client_id="parent-app",
            agent_session_id="parent-session",
            hop=1,
            trace_id="11111111111111111111111111111111",
        )
        coord = _coord(handler)
        self.assertIsNone(current())
        async with delegate_to_spawn(
            coordinator=coord,
            zone_id="z",
            application_id="child-app",
            subject_token="tok",
            scopes=["tool:call"],
            kind=AgentKind.SERVICE,
            parent_ctx=parent,
        ) as ctx:
            self.assertEqual(ctx.hop, 2)
            self.assertEqual(ctx.delegation_edge_id, "edge-9")
        self.assertEqual(seen["delegations"], 1)
        self.assertEqual(seen["agents"], 1)

    async def test_delegate_to_spawn_requires_parent_session(self) -> None:
        from caracalai_sdk.context import CaracalContext

        coord = _coord(_default_handler)
        bare = CaracalContext(
            subject_token="parent-tok",
            zone_id="z",
            client_id="parent-app",
            agent_session_id=None,
        )
        with self.assertRaises(RuntimeError):
            async with delegate_to_spawn(
                coordinator=coord,
                zone_id="z",
                application_id="child-app",
                subject_token="tok",
                scopes=["tool:call"],
                kind=AgentKind.SERVICE,
                parent_ctx=bare,
            ):
                pass


if __name__ == "__main__":
    unittest.main()
