"""Read-only chat sharing: the token, the owner check, the public payload.

The link is a capability — anyone holding it reads the chat and nothing else.
So these tests pin three things: one stable token per session, an owner-only
issuer, and a public payload that carries display history without leaking
ownership or the session id.
"""

from datetime import datetime, timezone
from types import SimpleNamespace

import pytest
from fastapi import HTTPException

import navi.api.routes.sessions as sessions_mod
import navi.api.routes.shared as shared_mod
from navi.auth import User
from navi.core.session import InMemorySessionStore, Session
from navi.exceptions import ProfileNotFound
from navi.llm.base import Message

_OWNER = User(
    id="owner", email="owner@example.com", display_name="Owner", role="user", permissions=[]
)
_STRANGER = User(
    id="stranger", email="s@example.com", display_name="S", role="user",
    permissions=["navi.sessions.read_all"],
)
_ADMIN = User(
    id="admin", email="a@example.com", display_name="A", role="admin", permissions=[]
)


def _session(user_id: str | None = "owner") -> Session:
    return Session(
        id="s-1",
        profile_id="assistant",
        user_id=user_id,
        name="A chat",
        created_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
        last_active=datetime(2026, 1, 2, tzinfo=timezone.utc),
    )


class _Store:
    """The slice of the store the endpoints touch, over a real in-memory one."""

    def __init__(self, session: Session | None, messages: list[Message] | None = None):
        self.session = session
        self.messages = messages or []
        self.memory = InMemorySessionStore()
        if session is not None:
            self.memory._sessions[session.id] = session

    async def get(self, session_id):
        return self.session if self.session and session_id == self.session.id else None

    async def get_meta(self, session_id):
        return await self.get(session_id)

    async def get_history_page(self, session_id, before_seq=None, limit=150):
        return list(self.messages), False

    async def get_or_create_share(self, session_id, owner_id):
        if await self.get(session_id) is None:
            return None
        return await self.memory.get_or_create_share(session_id, owner_id)

    async def get_share(self, token):
        return await self.memory.get_share(token)


class _Profiles:
    def __init__(self, name: str | None = "Assistant"):
        self.name = name

    def get(self, profile_id):
        if self.name is None:
            raise ProfileNotFound(profile_id)
        return SimpleNamespace(name=self.name)


# ── the store ───────────────────────────────────────────────────────────────


class TestInMemoryShares:
    async def test_one_session_hands_out_one_link(self):
        store = InMemorySessionStore()
        session = await store.create("assistant", user_id="owner")

        first = await store.get_or_create_share(session.id, "owner")
        second = await store.get_or_create_share(session.id, "owner")

        assert first["token"] == second["token"]
        assert len(first["token"]) >= 32

    async def test_a_token_resolves_back_to_its_session(self):
        store = InMemorySessionStore()
        session = await store.create("assistant", user_id="owner")
        share = await store.get_or_create_share(session.id, "owner")

        assert (await store.get_share(share["token"]))["session_id"] == session.id
        assert await store.get_share("never-issued") is None

    async def test_deleting_the_session_kills_the_link(self):
        store = InMemorySessionStore()
        session = await store.create("assistant", user_id="owner")
        share = await store.get_or_create_share(session.id, "owner")

        await store.delete(session.id)

        assert await store.get_share(share["token"]) is None

    async def test_unknown_session_has_no_link(self):
        assert await InMemorySessionStore().get_or_create_share("missing", None) is None


# ── issuing ─────────────────────────────────────────────────────────────────


class TestShareSession:
    def _settings(self, monkeypatch, *, auth: bool = True):
        monkeypatch.setattr(
            sessions_mod,
            "settings",
            SimpleNamespace(public_url="https://navi.example", navi_auth_enabled=auth),
        )

    async def test_the_owner_gets_a_link_to_the_read_only_page(self, monkeypatch):
        self._settings(monkeypatch)
        result = await sessions_mod.share_session("s-1", _Store(_session()), _OWNER)
        assert result["url"] == f"https://navi.example/#share={result['token']}"
        assert result["created_at"]

    async def test_asking_twice_returns_the_same_link(self, monkeypatch):
        self._settings(monkeypatch)
        store = _Store(_session())
        first = await sessions_mod.share_session("s-1", store, _OWNER)
        second = await sessions_mod.share_session("s-1", store, _OWNER)
        assert first["token"] == second["token"]

    async def test_a_stranger_with_read_all_may_read_but_not_share(self, monkeypatch):
        self._settings(monkeypatch)
        with pytest.raises(HTTPException) as exc:
            await sessions_mod.share_session("s-1", _Store(_session()), _STRANGER)
        assert exc.value.status_code == 403

    async def test_an_admin_may_share(self, monkeypatch):
        self._settings(monkeypatch)
        assert (await sessions_mod.share_session("s-1", _Store(_session()), _ADMIN))["token"]

    async def test_a_missing_session_is_404(self, monkeypatch):
        self._settings(monkeypatch)
        with pytest.raises(HTTPException) as exc:
            await sessions_mod.share_session("nope", _Store(_session()), _OWNER)
        assert exc.value.status_code == 404

    async def test_local_mode_shares_only_ownerless_sessions(self, monkeypatch):
        self._settings(monkeypatch, auth=False)
        result = await sessions_mod.share_session("s-1", _Store(_session(None)), _ADMIN)
        assert result["token"]
        with pytest.raises(HTTPException):
            await sessions_mod.share_session("s-1", _Store(_session("someone")), _ADMIN)


# ── the public payload ──────────────────────────────────────────────────────


class TestSharedChat:
    def _store(self) -> _Store:
        return _Store(_session(), [Message(role="user", content="hi", sequence_number=3)])

    async def _token(self, store) -> str:
        return (await store.get_or_create_share("s-1", None))["token"]

    async def test_history_is_served_without_auth(self):
        store = self._store()
        body = await shared_mod.get_shared_chat(await self._token(store), store, _Profiles(), 200)
        assert [m["content"] for m in body["messages"]] == ["hi"]
        assert body["name"] == "A chat"
        assert body["profile_name"] == "Assistant"

    async def test_the_payload_does_not_name_the_session_or_its_owner(self):
        store = self._store()
        body = await shared_mod.get_shared_chat(await self._token(store), store, _Profiles(), 200)
        assert "session_id" not in body
        assert "user_id" not in body

    async def test_a_missing_profile_does_not_break_the_page(self):
        store = self._store()
        body = await shared_mod.get_shared_chat(await self._token(store), store, _Profiles(None), 200)
        assert body["profile_name"] is None

    async def test_an_unknown_token_is_404(self):
        with pytest.raises(HTTPException) as exc:
            await shared_mod.get_shared_chat("nope", self._store(), _Profiles(), 200)
        assert exc.value.status_code == 404

    async def test_a_deleted_session_is_404(self):
        store = self._store()
        token = await self._token(store)
        store.session = None
        with pytest.raises(HTTPException) as exc:
            await shared_mod.get_shared_chat(token, store, _Profiles(), 200)
        assert exc.value.status_code == 404
