"""Tests for ratatoskr.first_message per docs/contracts/first_message.contract.md."""

import asyncio
import hashlib
import json

import httpx
import pytest
import respx

from ratatoskr.first_message import (
    FIRST_MESSAGE_PRESETS,
    preset_for,
    seed_preset_first_message,
)


class TestPresetFor:
    """first_message contract — preset_for (dict lookup)."""

    def test_preset_hit(self) -> None:
        """preset_hit [happy,tracer]: sindra has a non-empty str preset."""
        val = preset_for("ratatoskr:sindra")
        assert isinstance(val, str) and val

    def test_preset_miss(self) -> None:
        """preset_miss [happy]: an agent with no preset → None."""
        assert preset_for("mimir") is None

    def test_empty_agent_id(self) -> None:
        """empty_agent_id [adversarial]: "" → AssertionError."""
        with pytest.raises(AssertionError):
            preset_for("")


class TestSeedPresetFirstMessage:
    """first_message contract — seed_preset_first_message (best-effort #347 seed)."""

    @respx.mock
    async def test_seeds_preset(self) -> None:
        """seeds_preset [happy,tracer]: preset agent → one history POST, correct body."""
        content = FIRST_MESSAGE_PRESETS["ratatoskr:sindra"]
        key = "ratatoskr-preset-" + hashlib.sha256(content.encode("utf-8")).hexdigest()[:12]
        route = respx.post("https://w.example/sessions/s1/history").mock(
            return_value=httpx.Response(
                201,
                json={
                    "author": "assistant",
                    "seq": 0,
                    "phase": "seeded",
                    "turn_id": "t1",
                    "session_id": "s1",
                    "content_chars": len(content),
                    "injected_at": "2026-07-06T00:00:00+00:00",
                },
            )
        )
        async with httpx.AsyncClient(base_url="https://w.example") as client:
            result = await seed_preset_first_message(client, "s1", "ratatoskr:sindra")
        assert result == content
        assert route.call_count == 1  # POST-002: exactly one history POST
        assert json.loads(route.calls[0].request.content) == {
            "author": "assistant",
            "content": content,
            "idempotency_key": key,
        }

    @respx.mock
    async def test_no_preset_zero_http(self) -> None:
        """no_preset_zero_http [happy]: no-preset agent → None, ZERO HTTP (INV-002)."""
        route = respx.post("https://w.example/sessions/s1/history").mock(
            return_value=httpx.Response(201, json={})
        )
        async with httpx.AsyncClient(base_url="https://w.example") as client:
            result = await seed_preset_first_message(client, "s1", "mimir")
        assert result is None
        assert not route.called

    @respx.mock
    async def test_feature_absent_swallowed(self) -> None:
        """feature_absent_swallowed [error]: 404 hide-existence → None, no raise (INV-001)."""
        respx.post("https://w.example/sessions/s1/history").mock(
            return_value=httpx.Response(404, json={"error_code": "session_not_found"})
        )
        async with httpx.AsyncClient(base_url="https://w.example") as client:
            result = await seed_preset_first_message(client, "s1", "ratatoskr:sindra")
        assert result is None

    @respx.mock
    async def test_session_api_failed_swallowed(self) -> None:
        """session_api_failed_swallowed [error]: 409 → None, no raise (INV-001)."""
        respx.post("https://w.example/sessions/s1/history").mock(
            return_value=httpx.Response(409, json={"error_code": "generation_active"})
        )
        async with httpx.AsyncClient(base_url="https://w.example") as client:
            result = await seed_preset_first_message(client, "s1", "ratatoskr:sindra")
        assert result is None

    @respx.mock
    async def test_transport_error_swallowed(self) -> None:
        """transport_error_swallowed [error]: httpx.ConnectError → None, no raise (INV-001)."""
        respx.post("https://w.example/sessions/s1/history").mock(
            side_effect=httpx.ConnectError("boom")
        )
        async with httpx.AsyncClient(base_url="https://w.example") as client:
            result = await seed_preset_first_message(client, "s1", "ratatoskr:sindra")
        assert result is None

    @respx.mock
    async def test_unexpected_exception_swallowed(self) -> None:
        """unexpected_exception [error]: write raises ValueError → None (broad never-raise)."""
        respx.post("https://w.example/sessions/s1/history").mock(
            side_effect=ValueError("unexpected")
        )
        async with httpx.AsyncClient(base_url="https://w.example") as client:
            result = await seed_preset_first_message(client, "s1", "ratatoskr:sindra")
        assert result is None

    async def test_cancellation_propagates(self) -> None:
        """cancellation_propagates [error]: CancelledError from the write is RE-RAISED."""
        import ratatoskr.first_message as fm

        async def _cancel(*_a: object, **_k: object) -> None:
            raise asyncio.CancelledError

        orig = fm.write_authored_history
        fm.write_authored_history = _cancel  # type: ignore[assignment]
        try:
            async with httpx.AsyncClient(base_url="https://w.example") as client:
                with pytest.raises(asyncio.CancelledError):
                    await seed_preset_first_message(client, "s1", "ratatoskr:sindra")
        finally:
            fm.write_authored_history = orig  # type: ignore[assignment]

    @respx.mock
    async def test_malformed_agent_id_no_http(self) -> None:
        """malformed_agent_id [adversarial]: non-str or empty agent_id → None; no HTTP; no raise."""
        route = respx.post(url__regex=r".*/history$").mock(
            return_value=httpx.Response(201, json={})
        )
        async with httpx.AsyncClient(base_url="https://w.example") as client:
            assert await seed_preset_first_message(client, "s1", 123) is None  # type: ignore[arg-type]
            assert await seed_preset_first_message(client, "s1", "") is None
        assert not route.called

    @respx.mock
    async def test_empty_session_id(self) -> None:
        """empty_session_id [adversarial]: "" → None (soft guard); no HTTP; no raise."""
        route = respx.post("https://w.example/sessions/s1/history").mock(
            return_value=httpx.Response(201, json={})
        )
        async with httpx.AsyncClient(base_url="https://w.example") as client:
            result = await seed_preset_first_message(client, "", "ratatoskr:sindra")
        assert result is None
        assert not route.called
