# SPDX-License-Identifier: AGPL-3.0-only
# Copyright (c) 2026 sol pbc

"""Tests for the Yesterday's processing home card."""

from __future__ import annotations

import json
import shutil
from datetime import datetime
from pathlib import Path

import pytest

from solstone.apps.home.routes import (
    BRIEFING_LATENESS_THRESHOLD_HOURS,
    BRIEFING_MORNING_END_HOUR,
    _briefing_freshness,
    _briefing_lateness_state,
    _briefing_summary,
    _build_pulse_context,
    _collect_activities,
    _collect_anticipated_activities,
    _format_activity_label,
    _format_duration,
    _format_gap_links,
    _format_heatmap_summary,
    _newsletter_attempts_from_think_logs,
    _normalize_activity_title,
    _summarize_yesterday_processing,
)
from solstone.think.brain_health import HEADLINES

FIXTURES = Path(__file__).parent / "fixtures" / "journal"


def _brain_snapshot(state: str = "ready") -> dict:
    if state == "blocked":
        return {
            "state": "blocked",
            "headline": HEADLINES["blocked"],
            "progressing": False,
            "action": {"label": "open thinking", "href": "/app/thinking/#main"},
        }
    return {
        "state": "ready",
        "headline": HEADLINES["ready"],
        "progressing": False,
        "action": None,
    }


def _copy_fixture_file(journal: Path, rel_path: str) -> None:
    src = FIXTURES / rel_path
    dst = journal / rel_path
    dst.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy2(src, dst)


def _write_jsonl(path: Path, rows: list[dict]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(
        "".join(json.dumps(row, ensure_ascii=False) + "\n" for row in rows),
        encoding="utf-8",
    )


def _write_facet_meta(journal: Path, facet: str, title: str) -> None:
    path = journal / "facets" / facet / "facet.json"
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(
        json.dumps(
            {
                "title": title,
                "description": "",
                "color": "#0f172a",
                "emoji": "📁",
            }
        ),
        encoding="utf-8",
    )


def _seed_journal(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
    journal = tmp_path / "journal"
    journal.mkdir()
    monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal))

    for day, transcript_seconds in (("20260415", 3600), ("20260414", 2700)):
        facet_data = {"work": {"count": 1, "minutes": 15}}
        if day == "20260414":
            facet_data = {}
        stats_path = journal / "chronicle" / day / "stats.json"
        stats_path.parent.mkdir(parents=True, exist_ok=True)
        stats_path.write_text(
            json.dumps(
                {
                    "stats": {
                        "transcript_segments": 3,
                        "transcript_duration": transcript_seconds,
                    },
                    "facet_data": facet_data,
                    "heatmap_data": {"weekday": 2, "hours": {"9": 45.0}},
                }
            ),
            encoding="utf-8",
        )

    health_path = journal / "chronicle" / "20260415" / "health" / "100_daily.jsonl"
    health_path.parent.mkdir(parents=True, exist_ok=True)
    health_path.write_text("", encoding="utf-8")
    sparse_health_path = (
        journal / "chronicle" / "20260414" / "health" / "100_daily.jsonl"
    )
    sparse_health_path.parent.mkdir(parents=True, exist_ok=True)
    sparse_health_path.write_text(
        json.dumps({"event": "run.complete", "mode": "daily", "duration_ms": 10})
        + "\n",
        encoding="utf-8",
    )

    for rel_path in [
        "facets/work/activities/20260415.jsonl",
        "facets/personal/activities/20260415.jsonl",
        "facets/work/news/20260415.md",
        "facets/personal/news/20260415.md",
    ]:
        _copy_fixture_file(journal, rel_path)

    (journal / "chronicle" / "20260416").mkdir(parents=True, exist_ok=True)
    _write_facet_meta(journal, "work", "Work")
    _write_facet_meta(journal, "personal", "Personal")
    return journal


def _write_briefing(
    day: str,
    generated: str,
    *,
    payload: object | None = None,
) -> None:
    from solstone.think.talent import morning_briefing_path

    path = morning_briefing_path(day)
    path.parent.mkdir(parents=True, exist_ok=True)
    data = payload
    if data is None:
        data = {
            "metadata": {
                "generated": generated,
                "model": "test-model",
                "sources": {
                    "segments": 1,
                    "anticipated_activities": 1,
                    "facet_newsletters": 1,
                    "followups": 0,
                    "steward_health": "present",
                },
                "gaps": [],
                "coverage_preamble": "Built from test sources. No gaps.",
            },
            "your_day": [{"time": "", "text": "One thing."}],
            "yesterday": [],
            "needs_attention": [],
            "forward_look": [],
            "reading": [],
        }
    path.write_text(json.dumps(data), encoding="utf-8")


def _append_think_log(
    journal: Path,
    day: str,
    name: str,
    *,
    facet: str | None = None,
    event: str = "talent.fail",
) -> None:
    path = journal / "chronicle" / day / "health" / "101_daily.jsonl"
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("a", encoding="utf-8") as handle:
        record = {
            "event": event,
            "mode": "daily",
            "name": name,
            "state": "error",
            "ts": 1,
        }
        if facet is not None:
            record["facet"] = facet
        handle.write(json.dumps(record) + "\n")


def _patch_minimal_pulse_context(monkeypatch, pipeline_status):
    monkeypatch.setattr(
        "solstone.apps.home.routes.get_capture_health",
        lambda: {"status": "active", "observers": []},
    )
    monkeypatch.setattr("solstone.apps.home.routes.get_cached_state", lambda: {})
    monkeypatch.setattr("solstone.apps.home.routes.get_current", lambda: None)
    monkeypatch.setattr(
        "solstone.apps.home.routes._resolve_attention", lambda awareness: None
    )
    monkeypatch.setattr("solstone.apps.home.routes._today", lambda: "20260416")
    monkeypatch.setattr("solstone.apps.home.routes._yesterday", lambda: "20260415")
    monkeypatch.setattr(
        "solstone.apps.home.routes._count_journal_age_days", lambda today: 8
    )
    monkeypatch.setattr("solstone.apps.home.routes._load_stats", lambda today: {})
    monkeypatch.setattr(
        "solstone.apps.home.routes._load_flow_md", lambda today: (None, None)
    )
    monkeypatch.setattr(
        "solstone.apps.home.routes._load_pulse_narrative",
        lambda today: (None, None, []),
    )
    monkeypatch.setattr("solstone.apps.home.routes.load_briefing", lambda today: None)
    monkeypatch.setattr(
        "solstone.apps.home.routes._collect_anticipated_activities", lambda today: []
    )
    monkeypatch.setattr(
        "solstone.apps.home.routes._collect_activities", lambda today: []
    )
    monkeypatch.setattr(
        "solstone.apps.home.routes.read_steward_health",
        lambda: pipeline_status,
    )
    monkeypatch.setattr(
        "solstone.apps.home.routes.read_steward_summary",
        lambda *a, **k: None,
    )
    monkeypatch.setattr(
        "solstone.apps.home.routes.build_brain_snapshot",
        lambda *_a, **_k: _brain_snapshot(),
    )
    monkeypatch.setattr(
        "solstone.apps.home.routes._summarize_yesterday_processing",
        lambda yesterday, journal_age_days: {
            "title": "Yesterday's processing",
            "mode": "healthy",
            "default_collapsed": True,
            "summary_line": "I wrote 2 newsletters.",
            "details": [],
            "sparse_lines": None,
            "first_week_framing": None,
            "status_reasons": [],
        },
    )


def test_yesterdays_card_hidden_when_stats_missing(tmp_path, monkeypatch):
    _seed_journal(tmp_path, monkeypatch)
    _write_briefing("20260417", "2026-04-16T21:00:00")

    monkeypatch.setattr("solstone.apps.home.routes._today", lambda: "20260417")

    assert _summarize_yesterday_processing("20260416", 9) is None


def test_yesterdays_card_hidden_when_all_zero(tmp_path, monkeypatch):
    journal = _seed_journal(tmp_path, monkeypatch)
    _write_briefing("20260417", "2026-04-16T21:00:00")
    (journal / "chronicle" / "20260416" / "stats.json").write_text(
        json.dumps(
            {
                "stats": {
                    "transcript_segments": 0,
                    "transcript_duration": 0,
                },
                "facet_data": {},
                "heatmap_data": {"weekday": 3, "hours": {}},
            }
        ),
        encoding="utf-8",
    )

    monkeypatch.setattr("solstone.apps.home.routes._today", lambda: "20260417")

    assert _summarize_yesterday_processing("20260416", 9) is None


def test_collect_anticipated_activities_surfaces_only_anticipated_records(
    tmp_path,
    monkeypatch,
):
    journal = tmp_path / "journal"
    journal.mkdir()
    monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal))

    today = "20260418"
    now_ms = int(datetime.now().timestamp() * 1000)

    _write_facet_meta(journal, "work", "Work")
    _write_jsonl(
        journal / "facets" / "work" / "activities" / f"{today}.jsonl",
        [
            {
                "id": "anticipated_call_103000_0418",
                "activity": "call",
                "title": "Mari intro",
                "description": "Planned intro call",
                "target_date": "2026-04-18",
                "start": "10:30:00",
                "end": "11:00:00",
                "source": "anticipated",
                "created_at": now_ms,
                "participation": [
                    {
                        "name": "Mari Zumbro",
                        "role": "attendee",
                        "source": "screen",
                        "confidence": 0.9,
                        "context": "calendar invite",
                    },
                    {
                        "name": "Ramon",
                        "role": "mentioned",
                        "source": "screen",
                        "confidence": 0.6,
                        "context": "note",
                    },
                ],
            },
            {
                "id": "coding_090000_300",
                "activity": "coding",
                "title": "Focused coding",
                "description": "Recent work",
                "created_at": now_ms,
                "source": "user",
            },
        ],
    )

    anticipated_activities = _collect_anticipated_activities(today)
    activities = _collect_activities(today)

    assert [item["title"] for item in anticipated_activities] == ["Mari intro"]
    assert anticipated_activities[0]["occurred"] is False
    assert anticipated_activities[0]["participants"] == ["Mari Zumbro"]
    assert [activity["id"] for activity in activities] == ["coding_090000_300"]


def test_yesterdays_card_sparse_mode_copy(tmp_path, monkeypatch):
    _seed_journal(tmp_path, monkeypatch)
    _write_briefing("20260415", "2026-04-14T21:00:00")

    monkeypatch.setattr("solstone.apps.home.routes._today", lambda: "20260415")

    summary = _summarize_yesterday_processing("20260414", 2)

    assert summary["mode"] == "sparse"
    assert summary["default_collapsed"] is False
    assert summary["first_week_framing"] is None
    assert (
        summary["summary_line"]
        == "i took in 45 min yesterday and kept it in your journal."
    )
    assert summary["sparse_lines"] == [
        "I didn't produce any facet newsletters.",
        "There wasn't much else to process.",
    ]


def test_yesterdays_card_healthy_collapsed_on_day_8_plus(tmp_path, monkeypatch):
    _seed_journal(tmp_path, monkeypatch)
    _write_briefing("20260416", "2026-04-15T21:00:00")

    monkeypatch.setattr("solstone.apps.home.routes._today", lambda: "20260416")

    summary = _summarize_yesterday_processing("20260415", 8)

    assert summary["mode"] == "healthy"
    assert summary["title"] == "Yesterday's processing"
    assert summary["default_collapsed"] is True
    assert summary["first_week_framing"] is None
    assert (
        summary["summary_line"]
        == "I wrote 2 newsletters and prepared your morning briefing."
    )
    assert summary["gap_links"] == []


def test_yesterdays_card_healthy_expanded_with_framing_on_days_1_to_7(
    tmp_path, monkeypatch
):
    _seed_journal(tmp_path, monkeypatch)
    _write_briefing("20260416", "2026-04-15T21:00:00")

    monkeypatch.setattr("solstone.apps.home.routes._today", lambda: "20260416")

    summary = _summarize_yesterday_processing("20260415", 5)

    assert summary["mode"] == "healthy"
    assert summary["default_collapsed"] is False
    assert (
        summary["first_week_framing"]
        == "most of what i keep becomes useful after about a week, once your journal has enough of your days in it to show patterns. for now, here's what's already happening:"
    )


def test_yesterdays_card_degraded_shows_warning_and_partial_count(
    tmp_path, monkeypatch
):
    journal = _seed_journal(tmp_path, monkeypatch)
    _write_briefing("20260416", "2026-04-15T21:00:00")
    _append_think_log(journal, "20260415", "facet_newsletter", facet="personal")

    monkeypatch.setattr("solstone.apps.home.routes._today", lambda: "20260416")

    summary = _summarize_yesterday_processing("20260415", 8)

    assert summary["mode"] == "degraded"
    assert summary["title"] == "⚠ Yesterday's processing"
    assert summary["summary_line"] == (
        "I wrote 2 of 3 newsletters, but some overnight processing didn't finish."
    )
    assert summary["status_reasons"] == ["newsletter_partial", "pipeline_warning"]
    assert summary["gap_links"][0] == {
        "text": "The facet newsletter run didn't finish.",
        "href": "/app/sol/20260415#facet_newsletter",
    }
    assert "I wrote 2 of 3 newsletters." in summary["details"]


def test_yesterdays_card_degraded_zero_newsletters_keeps_failure_caveat(
    tmp_path, monkeypatch
):
    journal = _seed_journal(tmp_path, monkeypatch)
    for path in journal.glob("facets/*/news/20260415.md"):
        path.unlink()

    monkeypatch.setattr("solstone.apps.home.routes._today", lambda: "20260416")

    summary = _summarize_yesterday_processing("20260415", 8)

    assert summary["mode"] == "degraded"
    assert (
        summary["summary_line"]
        == "I didn't produce any facet newsletters, and some overnight processing didn't finish."
    )


def test_format_duration_boundaries():
    assert _format_duration(59) == "59 min"
    assert _format_duration(60) == "1 hour"


def test_heatmap_peaks_top_3():
    assert (
        _format_heatmap_summary(
            {
                "heatmap_data": {
                    "hours": {
                        "9": 45.0,
                        "11": 40.0,
                        "14": 30.0,
                        "10": 20.0,
                    }
                }
            }
        )
        == "your busiest stretches were 9-10am · 11am-12pm · 2-3pm."
    )


def test_activity_bullet_title_duration_facet(tmp_path, monkeypatch):
    _seed_journal(tmp_path, monkeypatch)
    monkeypatch.setattr("solstone.apps.home.routes._today", lambda: "20260416")
    activity = _summarize_yesterday_processing("20260415", 8)

    first_activity = next(
        detail for detail in activity["details"] if detail.startswith("I took notes on")
    )

    assert (
        first_activity
        == "I took notes on Extended coding session with Git and VS Code for 15 min in work."
    )
    assert (
        _format_activity_label(
            {
                "title": "Drafted weekend notes",
                "duration_minutes": 10,
                "facet": "personal",
            }
        )
        == "I took notes on Drafted weekend notes for 10 min in personal."
    )
    assert _normalize_activity_title({}) == "untitled activity"
    assert (
        _format_activity_label({"duration_minutes": 5})
        == "I took notes on untitled activity for 5 min in unknown."
    )


def test_briefing_missing_counts_as_gap(tmp_path, monkeypatch):
    _seed_journal(tmp_path, monkeypatch)

    monkeypatch.setattr("solstone.apps.home.routes._today", lambda: "20260416")

    summary = _summarize_yesterday_processing("20260415", 8)

    assert summary["mode"] == "degraded"
    assert "briefing_missing" in summary["status_reasons"]
    assert {
        "text": "I didn't prepare your morning briefing overnight.",
        "href": "/app/sol/20260416#morning_briefing",
    } in summary["gap_links"]
    assert _briefing_freshness("20260416") == {
        "exists": False,
        "valid": False,
        "generated_label": None,
    }


def test_briefing_freshness_valid_with_prior_evening_generated(tmp_path, monkeypatch):
    _seed_journal(tmp_path, monkeypatch)
    _write_briefing("20260416", "2026-04-15T21:30:00")

    result = _briefing_freshness("20260416")

    assert result == {"exists": True, "valid": True, "generated_label": "9:30pm"}


def test_briefing_summary_matches_fixture_projection(monkeypatch):
    from solstone.think.briefing import load_briefing, render_briefing_sections

    monkeypatch.setenv("SOLSTONE_JOURNAL", str(FIXTURES))

    briefing = load_briefing("20260327")
    assert briefing is not None
    sections = render_briefing_sections(briefing)

    assert (
        _briefing_summary(briefing, sections, len(briefing["needs_attention"]))
        == "morning briefing — 3 meetings, 3 items need attention"
    )


def test_briefing_freshness_invalid_when_missing(tmp_path, monkeypatch):
    _seed_journal(tmp_path, monkeypatch)

    assert _briefing_freshness("20260416") == {
        "exists": False,
        "valid": False,
        "generated_label": None,
    }


def test_briefing_freshness_invalid_when_root_is_list(tmp_path, monkeypatch):
    _seed_journal(tmp_path, monkeypatch)
    _write_briefing(
        "20260416",
        "2026-04-15T21:00:00",
        payload=[],
    )

    result = _briefing_freshness("20260416")

    assert result["exists"] is True
    assert result["valid"] is False


def test_briefing_freshness_invalid_when_metadata_missing(tmp_path, monkeypatch):
    _seed_journal(tmp_path, monkeypatch)
    _write_briefing(
        "20260416",
        "2026-04-15T21:00:00",
        payload={
            "your_day": [],
            "yesterday": [],
            "needs_attention": [],
            "forward_look": [],
            "reading": [],
        },
    )

    assert _briefing_freshness("20260416")["valid"] is False


def test_briefing_freshness_invalid_when_json_malformed(tmp_path, monkeypatch):
    from solstone.think.talent import morning_briefing_path

    _seed_journal(tmp_path, monkeypatch)
    path = morning_briefing_path("20260416")
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text("{not valid json", encoding="utf-8")

    assert _briefing_freshness("20260416") == {
        "exists": True,
        "valid": False,
        "generated_label": None,
    }


def test_gap_links_show_specific_daily_and_activity_without_generic():
    links = _format_gap_links(
        {
            "anomalies": [
                {"kind": "daily_agents_missing"},
                {"kind": "activity_agents_missing"},
                {"kind": "talent_failure"},
            ]
        },
        {"valid": True},
        "20260415",
        "20260416",
    )

    assert links == [
        {
            "text": "I didn't finish the full overnight review.",
            "href": "/app/sol/20260415",
        },
        {
            "text": "I didn't finish writing all of yesterday's notes.",
            "href": "/app/sol/20260415",
        },
    ]


@pytest.mark.parametrize(
    ("anomalies", "briefing", "expected"),
    [
        (
            [{"kind": "daily_agents_missing"}],
            {"valid": True},
            {
                "text": "I didn't finish the full overnight review.",
                "href": "/app/sol/20260415",
            },
        ),
        (
            [{"kind": "activity_agents_missing"}],
            {"valid": True},
            {
                "text": "I didn't finish writing all of yesterday's notes.",
                "href": "/app/sol/20260415",
            },
        ),
        (
            [{"kind": "talent_failure", "name": "flow", "use_id": "run-1"}],
            {"valid": True},
            {
                "text": "The flow run didn't finish.",
                "href": "/app/sol/20260415#flow/run-1",
            },
        ),
        (
            [
                {
                    "kind": "talent_failure",
                    "name": "flow",
                    "use_id": "run-1",
                    "state": "request_lost",
                }
            ],
            {"valid": True},
            {
                "text": "The flow run couldn't start.",
                "href": "/app/sol/20260415#flow/run-1",
            },
        ),
        (
            [{"kind": "talent_failure", "name": "facet_newsletter"}],
            {"valid": True},
            {
                "text": "The facet newsletter run didn't finish.",
                "href": "/app/sol/20260415#facet_newsletter",
            },
        ),
        (
            [{"kind": "talent_failure"}],
            {"valid": True},
            {
                "text": "Some of my overnight work didn't finish.",
                "href": "/app/sol/20260415",
            },
        ),
        (
            [],
            {"valid": False},
            {
                "text": "I didn't prepare your morning briefing overnight.",
                "href": "/app/sol/20260416#morning_briefing",
            },
        ),
    ],
)
def test_gap_links_href_for_each_anomaly_kind(anomalies, briefing, expected):
    assert expected in _format_gap_links(
        {"anomalies": anomalies},
        briefing,
        "20260415",
        "20260416",
    )


def test_gap_links_group_failures_by_name():
    links = _format_gap_links(
        {
            "anomalies": [
                {
                    "kind": "talent_failure",
                    "name": "facet_newsletter",
                    "use_id": "run-1",
                    "state": "timeout",
                },
                {
                    "kind": "talent_failure",
                    "name": "facet_newsletter",
                    "use_id": "run-2",
                    "state": "error",
                },
            ]
        },
        {"valid": True},
        "20260415",
        "20260416",
    )

    assert links == [
        {
            "text": "2 facet newsletter runs didn't finish.",
            "href": "/app/sol/20260415#facet_newsletter",
        }
    ]


def test_gap_links_group_request_lost_failures_by_name():
    links = _format_gap_links(
        {
            "anomalies": [
                {
                    "kind": "talent_failure",
                    "name": "flow",
                    "use_id": "run-1",
                    "state": "request_lost",
                },
                {
                    "kind": "talent_failure",
                    "name": "flow",
                    "use_id": "run-2",
                    "state": "request_lost",
                },
            ]
        },
        {"valid": True},
        "20260415",
        "20260416",
    )

    assert links == [
        {
            "text": "2 flow runs couldn't start.",
            "href": "/app/sol/20260415#flow",
        }
    ]


def test_gap_links_show_truncated_failures_count():
    anomalies = [
        {"kind": "talent_failure", "name": f"agent_{idx}", "state": "error"}
        for idx in range(20)
    ]

    links = _format_gap_links(
        {
            "anomalies": anomalies,
            "talents": {
                "outstanding_failed": 25,
                "failed_list_truncated": True,
            },
        },
        {"valid": True},
        "20260415",
        "20260416",
    )

    assert links[-1] == {
        "text": "…and 5 more didn't finish.",
        "href": "/app/sol/20260415",
    }


def test_briefing_lateness_threshold():
    due_hour = BRIEFING_MORNING_END_HOUR
    before = datetime(2026, 4, 16, due_hour + BRIEFING_LATENESS_THRESHOLD_HOURS, 45)
    after = datetime(2026, 4, 16, due_hour + BRIEFING_LATENESS_THRESHOLD_HOURS + 1, 0)

    assert _briefing_lateness_state(before, "pending") == {
        "late": False,
        "late_hours": 0,
    }
    assert _briefing_lateness_state(after, "pending") == {
        "late": True,
        "late_hours": BRIEFING_LATENESS_THRESHOLD_HOURS + 1,
    }
    assert _briefing_lateness_state(after, "active") == {
        "late": False,
        "late_hours": 0,
    }


def test_newsletter_attempts_option_a_matches_facet_newsletter_failures_only(
    tmp_path, monkeypatch
):
    journal = _seed_journal(tmp_path, monkeypatch)
    _append_think_log(journal, "20260415", "facet_newsletter", facet="work")
    _append_think_log(journal, "20260415", "flow", facet="work")
    _append_think_log(journal, "20260415", "facet_newsletter")

    assert _newsletter_attempts_from_think_logs("20260415") == (2, 3)


def test_build_pulse_context_includes_yesterday_processing(monkeypatch):
    monkeypatch.setattr(
        "solstone.apps.home.routes.get_capture_health",
        lambda: {"status": "active", "observers": []},
    )
    monkeypatch.setattr("solstone.apps.home.routes.get_cached_state", lambda: {})
    monkeypatch.setattr("solstone.apps.home.routes.get_current", lambda: None)
    monkeypatch.setattr(
        "solstone.apps.home.routes._resolve_attention", lambda awareness: None
    )
    monkeypatch.setattr("solstone.apps.home.routes._today", lambda: "20260416")
    monkeypatch.setattr("solstone.apps.home.routes._yesterday", lambda: "20260415")
    monkeypatch.setattr(
        "solstone.apps.home.routes._count_journal_age_days", lambda today: 8
    )
    monkeypatch.setattr("solstone.apps.home.routes._load_stats", lambda today: {})
    monkeypatch.setattr(
        "solstone.apps.home.routes._load_flow_md", lambda today: (None, None)
    )
    monkeypatch.setattr(
        "solstone.apps.home.routes._load_pulse_narrative",
        lambda today: (None, None, []),
    )
    monkeypatch.setattr("solstone.apps.home.routes.load_briefing", lambda today: None)
    monkeypatch.setattr(
        "solstone.apps.home.routes._collect_anticipated_activities", lambda today: []
    )
    monkeypatch.setattr(
        "solstone.apps.home.routes._collect_activities", lambda today: []
    )
    monkeypatch.setattr("solstone.apps.home.routes.read_steward_health", lambda: None)
    monkeypatch.setattr(
        "solstone.apps.home.routes.build_brain_snapshot",
        lambda *_a, **_k: _brain_snapshot(),
    )
    monkeypatch.setattr(
        "solstone.apps.home.routes._summarize_yesterday_processing",
        lambda yesterday, journal_age_days: {
            "title": "Yesterday's processing",
            "mode": "healthy",
            "default_collapsed": True,
            "summary_line": "I wrote 2 newsletters.",
            "details": [],
            "sparse_lines": None,
            "first_week_framing": None,
            "status_reasons": [],
        },
    )

    ctx = _build_pulse_context()

    assert ctx["yesterday_processing"]["summary_line"] == "I wrote 2 newsletters."


def test_build_pulse_context_pipeline_status_none_when_steward_healthy(monkeypatch):
    _patch_minimal_pulse_context(monkeypatch, None)

    ctx = _build_pulse_context()

    assert ctx["pipeline_status"] is None


def test_build_pulse_context_pipeline_status_surfaces_steward_warning(monkeypatch):
    status = {"status": "warning", "message": "Foo bar"}
    _patch_minimal_pulse_context(monkeypatch, status)

    ctx = _build_pulse_context()

    assert ctx["pipeline_status"] == status


def test_build_pulse_context_pipeline_status_enriched_with_summary(monkeypatch):
    status = {"status": "warning", "message": "Foo bar"}
    _patch_minimal_pulse_context(monkeypatch, status)
    monkeypatch.setattr(
        "solstone.apps.home.routes.read_steward_summary",
        lambda *a, **k: {
            "headline": "Pipeline gap",
            "summary_sentence": "Two segments awaiting thinking.",
            "suggested_action": "open_health_detail",
        },
    )

    ctx = _build_pulse_context()

    assert ctx["pipeline_status"]["headline"] == "Pipeline gap"
    assert (
        ctx["pipeline_status"]["summary_sentence"] == "Two segments awaiting thinking."
    )
    assert ctx["pipeline_status"]["suggested_action"] == "open_health_detail"
    # The deterministic warning signal is preserved.
    assert ctx["pipeline_status"]["message"] == "Foo bar"


def test_build_pulse_context_surfaces_brain_chip_when_blocked(monkeypatch):
    _patch_minimal_pulse_context(monkeypatch, None)
    monkeypatch.setattr(
        "solstone.apps.home.routes.build_brain_snapshot",
        lambda *_a, **_k: _brain_snapshot("blocked"),
    )

    ctx = _build_pulse_context()

    assert {
        "text": HEADLINES["blocked"],
        "severity": "amber",
        "href": "/app/thinking/#main",
    } in ctx["health_glance"]["issues"]
