from __future__ import annotations

from datetime import datetime, timezone
from types import SimpleNamespace

import httpx
import pytest

from witty_service.application.session_manager import (
    AGENT_NOT_FOUND,
    SESSION_AGENT_MISMATCH,
    SESSION_NOT_FOUND,
    SessionManager,
)
from witty_service.domain.errors import DomainError


class RepositoryStub:
    def __init__(self) -> None:
        self.agents = {"agent-1": SimpleNamespace(id="agent-1")}
        self.sessions = {}
        self.deleted = []
        self.upserts = []
        self.identity_updates = []

    def create_session(self, agent_id: str):
        session = SimpleNamespace(id="session-new", agent_id=agent_id)
        self.sessions[session.id] = session
        return session

    def get_session(self, session_id: str):
        return self.sessions.get(session_id)

    def list_sessions(self, agent_id: str):
        return [item for item in self.sessions.values() if item.agent_id == agent_id]

    def delete_session(self, session_id: str) -> None:
        self.deleted.append(session_id)
        self.sessions.pop(session_id, None)

    def upsert_session(self, **kwargs):
        self.upserts.append(kwargs)
        session = SimpleNamespace(
            id=kwargs["session_id"],
            agent_id=kwargs["agent_id"],
            status=kwargs["status"],
            context_initialized=kwargs.get("context_initialized", False),
            runtime_type=kwargs.get("runtime_type"),
            runtime_session_id=kwargs.get("runtime_session_id"),
            runtime_session_key=kwargs.get("runtime_session_key"),
            created_at=kwargs.get("created_at"),
            remote_runtime_agent_id=kwargs.get("remote_runtime_agent_id"),
        )
        self.sessions[session.id] = session
        return session

    def update_session_runtime_identity(self, **kwargs):
        self.identity_updates.append(kwargs)
        session = self.sessions[kwargs["session_id"]]
        payload = dict(session.__dict__)
        payload.update(
            runtime_type=kwargs["runtime_type"],
            runtime_session_id=kwargs["runtime_session_id"],
            runtime_session_key=kwargs["runtime_session_key"],
        )
        updated = SimpleNamespace(**payload)
        self.sessions[updated.id] = updated
        return updated

    def get_agent(self, agent_id: str):
        return self.agents.get(agent_id)


class AdaptorClientStub:
    def __init__(self) -> None:
        self.posts = []
        self.gets = []
        self.agents_payload = {"defaultId": "runtime-default"}
        self.sessions_payload = {
            "sessions": [
                {
                    "id": "remote-session-1",
                    "status": "idle",
                    "context_initialized": True,
                    "runtime_type": "openclaw",
                    "created_at": "2026-01-01T00:00:00+00:00",
                }
            ]
        }
        self.session_payload = {
            "id": "remote-session-1",
            "status": "running",
            "context_initialized": True,
            "runtime_type": "openclaw",
        }

    async def list_agents(self):
        return self.agents_payload

    async def post(self, path: str, json: dict):
        self.posts.append((path, json))
        if path.endswith("/sessions"):
            return {
                "id": "remote-session-1",
                "status": "idle",
                "context_initialized": True,
                "runtime_type": "openclaw",
                "created_at": "2026-01-01T00:00:00+00:00",
            }
        return {}

    async def get(self, path: str):
        self.gets.append(path)
        if path.endswith("/sessions"):
            return self.sessions_payload
        return self.session_payload


def test_create_get_list_and_delete_session() -> None:
    repo = RepositoryStub()
    manager = SessionManager(repo)

    created = manager.create_session("agent-1")
    fetched = manager.get_session("agent-1", created.id)
    listed = manager.list_sessions("agent-1")
    manager.delete_session("agent-1", created.id)

    assert fetched is created
    assert listed == [created]
    assert repo.deleted == [created.id]


def test_create_session_requires_existing_agent() -> None:
    manager = SessionManager(RepositoryStub())

    with pytest.raises(DomainError) as exc_info:
        manager.create_session("missing")

    assert exc_info.value.code == AGENT_NOT_FOUND


def test_get_session_raises_for_missing_session() -> None:
    manager = SessionManager(RepositoryStub())

    with pytest.raises(DomainError) as exc_info:
        manager.get_session("agent-1", "missing")

    assert exc_info.value.code == SESSION_NOT_FOUND


def test_get_session_rejects_agent_mismatch() -> None:
    repo = RepositoryStub()
    repo.agents["agent-2"] = SimpleNamespace(id="agent-2")
    repo.sessions["session-1"] = SimpleNamespace(id="session-1", agent_id="agent-2")
    manager = SessionManager(repo)

    with pytest.raises(DomainError) as exc_info:
        manager.get_session("agent-1", "session-1")

    assert exc_info.value.code == SESSION_AGENT_MISMATCH


@pytest.mark.asyncio
async def test_resolve_runtime_agent_id_priority_and_fallbacks() -> None:
    manager = SessionManager(RepositoryStub())
    client = AdaptorClientStub()

    assert await manager.resolve_runtime_agent_id(client, "explicit") == "explicit"
    assert await manager.resolve_runtime_agent_id(client) == "runtime-default"

    client.agents_payload = {"agents": [{"id": "runtime-2", "default": True}]}
    assert await manager.resolve_runtime_agent_id(client) == "runtime-2"

    client.agents_payload = {"agents": []}
    with pytest.raises(DomainError) as exc_info:
        await manager.resolve_runtime_agent_id(client)
    assert exc_info.value.code == "RUNTIME_AGENT_DEFAULT_NOT_FOUND"


@pytest.mark.asyncio
async def test_remote_session_methods_sync_repository() -> None:
    repo = RepositoryStub()
    manager = SessionManager(repo)
    client = AdaptorClientStub()

    created = await manager.create_session_remote("agent-1", client)
    listed = await manager.list_sessions_remote("agent-1", client, "runtime-explicit")
    fetched = await manager.get_session_remote("agent-1", created.id, client)
    await manager.abort_session_remote("agent-1", created.id, client)
    await manager.delete_session_remote("agent-1", created.id, client)

    assert created.remote_runtime_agent_id == "runtime-default"
    assert listed[0].id == "remote-session-1"
    assert fetched.status == "running"
    assert client.posts == [
        ("/agents/runtime-default/sessions", {}),
        ("/agents/runtime-explicit/sessions/remote-session-1/abort", {}),
        ("/agents/runtime-explicit/sessions/remote-session-1/delete", {}),
    ]
    assert client.gets == [
        "/agents/runtime-explicit/sessions",
        "/agents/runtime-explicit/sessions/remote-session-1",
    ]
    assert repo.deleted == ["remote-session-1"]


@pytest.mark.asyncio
async def test_get_session_remote_404_cleans_local_session_cache() -> None:
    repo = RepositoryStub()
    manager = SessionManager(repo)
    client = AdaptorClientStub()
    repo.upsert_session(
        session_id="remote-session-404",
        agent_id="agent-1",
        status="idle",
        remote_runtime_agent_id="runtime-explicit",
    )

    async def raise_404(path: str):
        request = httpx.Request("GET", f"https://example.test{path}")
        response = httpx.Response(404, request=request)
        raise httpx.HTTPStatusError("not found", request=request, response=response)

    client.get = raise_404

    with pytest.raises(DomainError) as exc_info:
        await manager.get_session_remote("agent-1", "remote-session-404", client)

    assert exc_info.value.code == SESSION_NOT_FOUND
    assert repo.deleted == ["remote-session-404"]


def test_upsert_session_delegates_all_fields() -> None:
    repo = RepositoryStub()
    manager = SessionManager(repo)
    created_at = datetime.now(timezone.utc)

    session = manager.upsert_session(
        session_id="session-1",
        agent_id="agent-1",
        status="idle",
        context_initialized=True,
        runtime_type="openclaw",
        created_at=created_at,
        remote_runtime_agent_id="runtime-1",
    )

    assert session.id == "session-1"
    assert repo.upserts[-1] == {
        "session_id": "session-1",
        "agent_id": "agent-1",
        "status": "idle",
        "context_initialized": True,
        "runtime_type": "openclaw",
        "runtime_session_key": None,
        "created_at": created_at,
        "remote_runtime_agent_id": "runtime-1",
    }


def test_update_session_runtime_identity_delegates_to_repository() -> None:
    repo = RepositoryStub()
    manager = SessionManager(repo)
    repo.upsert_session(
        session_id="session-1",
        agent_id="agent-1",
        status="idle",
    )

    session = manager.update_session_runtime_identity(
        session_id="session-1",
        runtime_type="openclaw",
        runtime_session_id="runtime-session-1",
        runtime_session_key="agent:agent-1:session:session-1",
    )

    assert session.runtime_type == "openclaw"
    assert session.runtime_session_id == "runtime-session-1"
    assert session.runtime_session_key == "agent:agent-1:session:session-1"
    assert repo.identity_updates[-1] == {
        "session_id": "session-1",
        "runtime_type": "openclaw",
        "runtime_session_id": "runtime-session-1",
        "runtime_session_key": "agent:agent-1:session:session-1",
    }