"""Offline SDK contracts and lifecycle fixtures. No provider/network session."""

import asyncio
import inspect
import unittest
from unittest.mock import patch

from livekit.agents import AgentSession, JobContext
from livekit.plugins import lemonslice
from livekit_lemonslice import AvatarConfig, start_avatar


CONFIG = AvatarConfig(
    image_url="https://example.com/avatar.jpg",
    api_key="offline-provider-placeholder",
    livekit_url="wss://example.com",
    livekit_api_key="offline-server-placeholder",
    livekit_api_secret="offline-secret-placeholder",
)


class LifecycleFixture:
    def __init__(self, failure=None, delay=0):
        self.events = []
        self.failure = failure
        self.delay = delay
        self.callbacks = []
        self.room = self

    async def connect(self):
        self.events.append("connect")

    def add_shutdown_callback(self, callback):
        self.events.append("register_cleanup")
        self.callbacks.append(callback)

    async def disconnect(self):
        self.events.append("disconnect")

    def avatar_factory(self, **kwargs):
        self.kwargs = kwargs
        outer = self

        class Avatar:
            async def start(self, session, room, **credentials):
                outer.events.append("avatar_start")
                outer.credentials = credentials
                await asyncio.sleep(outer.delay)
                if outer.failure:
                    raise outer.failure
                return "offline-session"

            async def aclose(self):
                outer.events.append("avatar_close")

        return Avatar()

    async def start(self, *, agent, room):
        self.events.append("agent_start")


class Tests(unittest.IsolatedAsyncioTestCase):
    async def asyncSetUp(self):
        # Fail any accidental Python socket connection after SDK imports.
        self.guard = patch("socket.socket.connect", side_effect=AssertionError("Network forbidden"))
        self.guard.start()

    async def asyncTearDown(self):
        self.guard.stop()

    async def test_installed_sdk_contract(self):
        constructor = inspect.signature(lemonslice.AvatarSession)
        for field in ("api_key", "agent_image_url", "idle_timeout"):
            self.assertIn(field, constructor.parameters)
        for field in ("livekit_url", "livekit_api_key", "livekit_api_secret", "room"):
            self.assertIn(field, inspect.signature(lemonslice.AvatarSession.start).parameters)
        self.assertTrue(inspect.iscoroutinefunction(lemonslice.AvatarSession.aclose))
        self.assertTrue(callable(JobContext.add_shutdown_callback))
        self.assertIn("room", inspect.signature(AgentSession.start).parameters)
        avatar = lemonslice.AvatarSession(
            api_key=CONFIG.api_key, agent_image_url=CONFIG.image_url,
            idle_timeout=60, edit_image=False,
        )
        self.assertEqual(avatar.provider, "lemonslice")
        await avatar.aclose()

    async def test_success_order_and_idempotent_shutdown(self):
        fixture = LifecycleFixture()
        await start_avatar(fixture, fixture, object(), CONFIG, avatar_factory=fixture.avatar_factory)
        self.assertEqual(fixture.events, ["connect", "register_cleanup", "avatar_start", "agent_start"])
        self.assertEqual(fixture.kwargs["idle_timeout"], 60)
        self.assertFalse(fixture.kwargs["edit_image"])
        await fixture.callbacks[0]("user left")
        await fixture.callbacks[0]("duplicate shutdown")
        self.assertEqual(fixture.events.count("avatar_close"), 1)

    async def test_failed_start_closes_avatar_and_room(self):
        fixture = LifecycleFixture(failure=RuntimeError("offline rejected start"))
        with self.assertRaisesRegex(RuntimeError, "offline rejected"):
            await start_avatar(fixture, fixture, object(), CONFIG, avatar_factory=fixture.avatar_factory)
        self.assertEqual(fixture.events[-2:], ["avatar_close", "disconnect"])
        self.assertNotIn("agent_start", fixture.events)

    async def test_timeout_closes_avatar_and_room(self):
        fixture = LifecycleFixture(delay=0.1)
        with self.assertRaises(TimeoutError):
            await start_avatar(
                fixture, fixture, object(), CONFIG, startup_timeout=0.01,
                avatar_factory=fixture.avatar_factory,
            )
        self.assertEqual(fixture.events[-2:], ["avatar_close", "disconnect"])

    async def test_keys_redacted_and_bad_configuration_rejected(self):
        self.assertNotIn(CONFIG.api_key, repr(CONFIG))
        self.assertNotIn(CONFIG.livekit_api_secret, repr(CONFIG))
        with self.assertRaises(ValueError):
            AvatarConfig("http://example.com/avatar.jpg", "x", "wss://example.com", "x", "x")
        with self.assertRaises(ValueError):
            AvatarConfig("https://example.com/avatar.jpg", "", "wss://example.com", "x", "x")


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