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

"""Tests for think --activity mode and activity template variables."""

import json
import tempfile
from pathlib import Path

import pytest

# ---------------------------------------------------------------------------
# run_activity_prompts
# ---------------------------------------------------------------------------


class TestRunActivityPrompts:
    """Tests for think.run_activity_prompts."""

    def _write_record(self, tmpdir, facet, day, record):
        """Helper to write an activity record to the journal."""
        path = Path(tmpdir) / "facets" / facet / "activities" / f"{day}.jsonl"
        path.parent.mkdir(parents=True, exist_ok=True)
        with open(path, "a") as f:
            f.write(json.dumps(record) + "\n")

    def test_not_found_returns_false(self, monkeypatch):
        from solstone.think.thinking import run_activity_prompts

        with tempfile.TemporaryDirectory() as tmpdir:
            monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)

            result = run_activity_prompts(
                day="20260209",
                activity_id="nonexistent_100000_300",
                facet="work",
            )
            assert result is False

    def test_no_matching_agents_returns_true(self, monkeypatch):
        from solstone.think.thinking import run_activity_prompts

        with tempfile.TemporaryDirectory() as tmpdir:
            monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)

            self._write_record(
                tmpdir,
                "work",
                "20260209",
                {
                    "id": "coding_100000_300",
                    "activity": "coding",
                    "segments": ["100000_300", "100500_300"],
                    "level_avg": 0.75,
                    "description": "Coding session",
                    "active_entities": ["VS Code"],
                },
            )

            # No activity-scheduled agents
            monkeypatch.setattr(
                "solstone.think.thinking.get_talent_configs", lambda schedule: {}
            )

            result = run_activity_prompts(
                day="20260209",
                activity_id="coding_100000_300",
                facet="work",
            )
            assert result is True

    def test_filters_by_activity_type(self, monkeypatch):
        from solstone.think.thinking import run_activity_prompts

        with tempfile.TemporaryDirectory() as tmpdir:
            monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)

            self._write_record(
                tmpdir,
                "work",
                "20260209",
                {
                    "id": "coding_100000_300",
                    "activity": "coding",
                    "segments": ["100000_300"],
                    "level_avg": 0.75,
                    "description": "Coding session",
                    "active_entities": [],
                },
            )

            configs = {
                "code_review": {
                    "type": "cogitate",
                    "priority": 50,
                    "activities": ["coding"],
                    "multi_facet": True,
                },
                "meeting_notes": {
                    "type": "generate",
                    "priority": 50,
                    "activities": ["meeting"],
                    "output": "md",
                },
            }

            monkeypatch.setattr(
                "solstone.think.thinking.get_talent_configs", lambda schedule: configs
            )

            spawned_requests = []

            def mock_cortex_request(prompt, name, config, **kwargs):
                spawned_requests.append((name, config))
                return f"agent-{name}"

            monkeypatch.setattr(
                "solstone.think.thinking.cortex_request", mock_cortex_request
            )
            monkeypatch.setattr(
                "solstone.think.thinking.wait_for_uses",
                lambda ids, timeout: ({aid: "finish" for aid in ids}, []),
            )

            result = run_activity_prompts(
                day="20260209",
                activity_id="coding_100000_300",
                facet="work",
            )

            assert result is True
            # Only code_review should be spawned (matches "coding" type)
            assert len(spawned_requests) == 1
            assert spawned_requests[0][0] == "code_review"

    def test_wildcard_matches_all_types(self, monkeypatch):
        from solstone.think.thinking import run_activity_prompts

        with tempfile.TemporaryDirectory() as tmpdir:
            monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)

            self._write_record(
                tmpdir,
                "work",
                "20260209",
                {
                    "id": "browsing_100000_300",
                    "activity": "browsing",
                    "segments": ["100000_300"],
                    "level_avg": 0.5,
                    "description": "Web browsing",
                    "active_entities": [],
                },
            )

            configs = {
                "activity_summary": {
                    "type": "generate",
                    "priority": 50,
                    "activities": ["*"],
                    "output": "md",
                },
            }

            monkeypatch.setattr(
                "solstone.think.thinking.get_talent_configs", lambda schedule: configs
            )

            spawned = []

            def mock_cortex_request(prompt, name, config, **kwargs):
                spawned.append(name)
                return f"agent-{name}"

            monkeypatch.setattr(
                "solstone.think.thinking.cortex_request", mock_cortex_request
            )
            monkeypatch.setattr(
                "solstone.think.thinking.wait_for_uses",
                lambda ids, timeout: ({aid: "finish" for aid in ids}, []),
            )

            result = run_activity_prompts(
                day="20260209",
                activity_id="browsing_100000_300",
                facet="work",
            )

            assert result is True
            assert spawned == ["activity_summary"]

    def test_passes_span_and_activity_in_request(self, monkeypatch):
        from solstone.think.thinking import run_activity_prompts

        with tempfile.TemporaryDirectory() as tmpdir:
            monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)

            record = {
                "id": "coding_100000_300",
                "activity": "coding",
                "segments": ["100000_300", "100500_300", "101000_300"],
                "level_avg": 0.83,
                "description": "Extended coding",
                "active_entities": ["VS Code", "Git"],
            }
            self._write_record(tmpdir, "work", "20260209", record)

            configs = {
                "code_review": {
                    "type": "generate",
                    "priority": 50,
                    "activities": ["coding"],
                    "output": "md",
                },
            }

            monkeypatch.setattr(
                "solstone.think.thinking.get_talent_configs", lambda schedule: configs
            )

            captured_config = {}

            def mock_cortex_request(prompt, name, config, **kwargs):
                captured_config.update(config)
                return "agent-1"

            monkeypatch.setattr(
                "solstone.think.thinking.cortex_request", mock_cortex_request
            )
            monkeypatch.setattr(
                "solstone.think.thinking.wait_for_uses",
                lambda ids, timeout: ({aid: "finish" for aid in ids}, []),
            )

            run_activity_prompts(
                day="20260209",
                activity_id="coding_100000_300",
                facet="work",
            )

            assert captured_config["span"] == [
                "100000_300",
                "100500_300",
                "101000_300",
            ]
            assert captured_config["facet"] == "work"
            assert captured_config["day"] == "20260209"
            assert captured_config["activity"]["id"] == "coding_100000_300"
            assert captured_config["activity"]["activity"] == "coding"

            # Verify facet-scoped output_path
            assert "output_path" in captured_config
            output_path = captured_config["output_path"]
            assert "facets/work/activities/20260209/coding_100000_300/" in output_path
            assert output_path.endswith("code_review.md")

    def test_failed_agent_returns_false(self, monkeypatch):
        from solstone.think.thinking import run_activity_prompts

        with tempfile.TemporaryDirectory() as tmpdir:
            monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)

            self._write_record(
                tmpdir,
                "work",
                "20260209",
                {
                    "id": "coding_100000_300",
                    "activity": "coding",
                    "segments": ["100000_300"],
                    "level_avg": 0.75,
                    "description": "Coding",
                    "active_entities": [],
                },
            )

            configs = {
                "code_review": {
                    "type": "cogitate",
                    "priority": 50,
                    "activities": ["coding"],
                },
            }

            monkeypatch.setattr(
                "solstone.think.thinking.get_talent_configs", lambda schedule: configs
            )
            monkeypatch.setattr(
                "solstone.think.thinking.cortex_request",
                lambda prompt, name, config, **kwargs: "agent-1",
            )
            monkeypatch.setattr(
                "solstone.think.thinking.wait_for_uses",
                lambda ids, timeout: ({aid: "error" for aid in ids}, []),
            )

            result = run_activity_prompts(
                day="20260209",
                activity_id="coding_100000_300",
                facet="work",
            )

            assert result is False

    def test_empty_segments_returns_true_for_synthetic_record(self, monkeypatch):
        from solstone.think.thinking import run_activity_prompts

        with tempfile.TemporaryDirectory() as tmpdir:
            monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)

            self._write_record(
                tmpdir,
                "work",
                "20260209",
                {
                    "id": "coding_100000_300",
                    "activity": "coding",
                    "segments": [],
                    "level_avg": 0.5,
                    "description": "Empty",
                    "active_entities": [],
                },
            )

            result = run_activity_prompts(
                day="20260209",
                activity_id="coding_100000_300",
                facet="work",
            )

            assert result is True

    def test_cogitate_source_returns_true_without_running_agents(self, monkeypatch):
        from solstone.think.thinking import run_activity_prompts

        with tempfile.TemporaryDirectory() as tmpdir:
            monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)

            self._write_record(
                tmpdir,
                "work",
                "20260209",
                {
                    "id": "coding_100000_300",
                    "activity": "coding",
                    "source": "cogitate",
                    "segments": ["100000_300"],
                    "level_avg": 0.5,
                    "description": "Synthetic",
                    "active_entities": [],
                },
            )

            monkeypatch.setattr(
                "solstone.think.thinking.get_talent_configs",
                lambda schedule: {"session_review": {"activities": ["*"]}},
            )

            result = run_activity_prompts(
                day="20260209",
                activity_id="coding_100000_300",
                facet="work",
            )

            assert result is True

    def test_emits_think_events(self, monkeypatch):
        from solstone.think.thinking import run_activity_prompts

        with tempfile.TemporaryDirectory() as tmpdir:
            monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)

            self._write_record(
                tmpdir,
                "work",
                "20260209",
                {
                    "id": "coding_100000_300",
                    "activity": "coding",
                    "segments": ["100000_300"],
                    "level_avg": 0.75,
                    "description": "Coding",
                    "active_entities": [],
                },
            )

            configs = {
                "code_review": {
                    "type": "cogitate",
                    "priority": 50,
                    "activities": ["*"],
                },
            }

            monkeypatch.setattr(
                "solstone.think.thinking.get_talent_configs", lambda schedule: configs
            )
            monkeypatch.setattr(
                "solstone.think.thinking.cortex_request",
                lambda prompt, name, config, **kwargs: "agent-1",
            )
            monkeypatch.setattr(
                "solstone.think.thinking.wait_for_uses",
                lambda ids, timeout: ({aid: "finish" for aid in ids}, []),
            )

            emitted = []
            monkeypatch.setattr(
                "solstone.think.thinking.emit",
                lambda event, **kw: emitted.append((event, kw)),
            )

            run_activity_prompts(
                day="20260209",
                activity_id="coding_100000_300",
                facet="work",
            )

            events = [e[0] for e in emitted]
            assert "started" in events
            assert "group_started" in events
            assert "talent_started" in events
            assert "talent_completed" in events

            assert "group_completed" in events
            assert "completed" in events

            # Verify mode=activity in started event
            started_kw = next(kw for ev, kw in emitted if ev == "started")
            assert started_kw["mode"] == "activity"
            assert started_kw["activity"] == "coding_100000_300"
            assert started_kw["facet"] == "work"

    @pytest.mark.parametrize(
        ("activity_type", "expected_name"),
        [
            ("meeting", "conversation"),
            ("call", "conversation"),
            ("messaging", "conversation"),
            ("email", "conversation"),
            ("appointment", "event"),
            ("event", "event"),
            ("travel", "event"),
            ("errand", "event"),
            ("celebration", "event"),
            ("deadline", "event"),
            ("reminder", "event"),
        ],
    )
    def test_dispatches_storytelling_talents_by_activity_type(
        self, monkeypatch, activity_type, expected_name
    ):
        from solstone.think.thinking import run_activity_prompts

        with tempfile.TemporaryDirectory() as tmpdir:
            monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)

            activity_id = f"{activity_type}_100000_300"
            self._write_record(
                tmpdir,
                "work",
                "20260209",
                {
                    "id": activity_id,
                    "activity": activity_type,
                    "segments": ["100000_300"],
                    "description": f"{activity_type} session",
                    "active_entities": [],
                },
            )

            configs = {
                "conversation": {
                    "type": "generate",
                    "priority": 50,
                    "activities": ["meeting", "call", "messaging", "email"],
                    "output": "json",
                },
                "work": {
                    "type": "generate",
                    "priority": 50,
                    "activities": ["coding", "browsing", "reading"],
                    "output": "json",
                },
                "event": {
                    "type": "generate",
                    "priority": 50,
                    "activities": [
                        "appointment",
                        "event",
                        "travel",
                        "errand",
                        "celebration",
                        "deadline",
                        "reminder",
                    ],
                    "output": "json",
                },
            }

            monkeypatch.setattr(
                "solstone.think.thinking.get_talent_configs", lambda schedule: configs
            )

            spawned: list[str] = []

            def mock_cortex_request(prompt, name, config, **kwargs):
                spawned.append(name)
                return f"agent-{name}"

            monkeypatch.setattr(
                "solstone.think.thinking.cortex_request", mock_cortex_request
            )
            monkeypatch.setattr(
                "solstone.think.thinking.wait_for_uses",
                lambda ids, timeout: ({aid: "finish" for aid in ids}, []),
            )

            result = run_activity_prompts(
                day="20260209",
                activity_id=activity_id,
                facet="work",
            )

            assert result is True
            assert spawned == [expected_name]

    def test_dispatches_work_talent_with_level_gate(self, monkeypatch):
        from solstone.think.thinking import run_activity_prompts

        with tempfile.TemporaryDirectory() as tmpdir:
            monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)

            configs = {
                "work": {
                    "type": "generate",
                    "priority": 50,
                    "activities": ["coding", "browsing", "reading"],
                    "output": "json",
                }
            }
            monkeypatch.setattr(
                "solstone.think.thinking.get_talent_configs", lambda schedule: configs
            )
            monkeypatch.setattr(
                "solstone.think.thinking.wait_for_uses",
                lambda ids, timeout: ({aid: "finish" for aid in ids}, []),
            )

            cases = [
                ("coding_100000_300", "coding", None, True),
                ("browsing_100000_300", "browsing", 0.4, True),
                ("reading_100000_300", "reading", 0.8, True),
                ("browsing_100500_300", "browsing", 0.39, False),
                ("reading_101000_300", "reading", None, False),
            ]

            for activity_id, activity_type, level_avg, should_spawn in cases:
                self._write_record(
                    tmpdir,
                    "work",
                    "20260209",
                    {
                        "id": activity_id,
                        "activity": activity_type,
                        "segments": ["100000_300"],
                        "level_avg": level_avg,
                        "description": activity_type,
                        "active_entities": [],
                    },
                )

            spawned: list[str] = []

            def mock_cortex_request(prompt, name, config, **kwargs):
                spawned.append(config["activity"]["id"])
                return f"agent-{config['activity']['id']}"

            monkeypatch.setattr(
                "solstone.think.thinking.cortex_request", mock_cortex_request
            )

            for activity_id, _activity_type, _level_avg, _should_spawn in cases:
                result = run_activity_prompts(
                    day="20260209",
                    activity_id=activity_id,
                    facet="work",
                )
                assert result is True

            assert spawned == [
                "coding_100000_300",
                "browsing_100000_300",
                "reading_100000_300",
            ]

    @pytest.mark.parametrize(
        "record",
        [
            {
                "id": "coding_100000_300",
                "activity": "coding",
                "source": "cogitate",
                "segments": ["100000_300"],
            },
            {
                "id": "coding_100500_300",
                "activity": "coding",
                "source": "anticipated",
                "segments": ["100500_300"],
            },
            {
                "id": "coding_101000_300",
                "activity": "coding",
                "segments": [],
            },
        ],
    )
    def test_storytelling_talents_skip_synthetic_or_empty_records(
        self, monkeypatch, record
    ):
        from solstone.think.thinking import run_activity_prompts

        with tempfile.TemporaryDirectory() as tmpdir:
            monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)

            full_record = {
                "description": "skip me",
                "active_entities": [],
                **record,
            }
            self._write_record(tmpdir, "work", "20260209", full_record)

            monkeypatch.setattr(
                "solstone.think.thinking.get_talent_configs",
                lambda schedule: {
                    "conversation": {
                        "type": "generate",
                        "priority": 50,
                        "activities": ["meeting", "call", "messaging", "email"],
                        "output": "json",
                    },
                    "work": {
                        "type": "generate",
                        "priority": 50,
                        "activities": ["coding", "browsing", "reading"],
                        "output": "json",
                    },
                    "event": {
                        "type": "generate",
                        "priority": 50,
                        "activities": [
                            "appointment",
                            "event",
                            "travel",
                            "errand",
                            "celebration",
                            "deadline",
                            "reminder",
                        ],
                        "output": "json",
                    },
                },
            )
            monkeypatch.setattr(
                "solstone.think.thinking.cortex_request",
                lambda prompt, name, config, **kwargs: pytest.fail(
                    "should not dispatch"
                ),
            )

            result = run_activity_prompts(
                day="20260209",
                activity_id=record["id"],
                facet="work",
            )

            assert result is True


class TestActivityPersistence:
    """Verify state machine completed records persist and load correctly."""

    def test_completed_record_persisted_and_found(self, monkeypatch):
        import tempfile

        from solstone.think.activities import (
            append_activity_record,
            load_activity_records,
        )
        from solstone.think.activity_state_machine import ActivityStateMachine

        with tempfile.TemporaryDirectory() as tmpdir:
            monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)

            sm = ActivityStateMachine()
            sm.update(
                {
                    "density": "active",
                    "content_type": "coding",
                    "activity_summary": "Writing tests",
                    "entities": [],
                    "facets": [
                        {"facet": "work", "activity": "coding", "level": "high"}
                    ],
                    "meeting_detected": False,
                    "speakers": [],
                    "recommend": {},
                },
                "090000_300",
                "20260304",
            )
            sm.update(
                {
                    "density": "active",
                    "content_type": "meeting",
                    "activity_summary": "Stand-up",
                    "entities": [],
                    "facets": [
                        {"facet": "work", "activity": "meeting", "level": "medium"}
                    ],
                    "meeting_detected": True,
                    "speakers": [],
                    "recommend": {},
                },
                "090500_300",
                "20260304",
            )
            changes = sm.update(
                {
                    "density": "active",
                    "content_type": "meeting",
                    "activity_summary": "Stand-up",
                    "entities": [],
                    "facets": [
                        {"facet": "work", "activity": "meeting", "level": "medium"}
                    ],
                    "meeting_detected": True,
                    "speakers": [],
                    "recommend": {},
                },
                "091500_300",
                "20260304",
            )

            # Find the ended change
            ended = [c for c in changes if c.get("state") == "ended"]
            assert len(ended) == 1
            facet = ended[0]["facet"]

            # Persist completed record (what thinking.py now does)
            completed = sm.get_completed_activities()
            assert len(completed) == 1
            rec = completed[0]
            assert isinstance(rec["created_at"], int)
            append_activity_record(facet, "20260304", rec)

            # Verify load finds it (what run_activity_prompts does)
            records = load_activity_records(facet, "20260304")
            assert len(records) == 1
            assert records[0]["id"] == rec["id"]
            assert records[0]["activity"] == "coding"
            assert isinstance(records[0]["created_at"], int)


class TestActivityPersistenceRoundTrip:
    """Full round-trip: state machine → append → load → field verification."""

    def _sense(
        self,
        content_type="coding",
        density="active",
        facets=None,
        summary="Working.",
        entities=None,
    ):
        if facets is None:
            facets = [{"facet": "work", "activity": content_type, "level": "high"}]
        return {
            "density": density,
            "content_type": content_type,
            "activity_summary": summary,
            "entities": entities or [],
            "facets": facets,
            "meeting_detected": False,
            "speakers": [],
            "recommend": {},
        }

    def _active_machine(
        self,
        segments,
        *,
        day="20260412",
        content_type="meeting",
        facet="work",
    ):
        from solstone.think.activity_state_machine import ActivityStateMachine

        sm = ActivityStateMachine()
        facets = [{"facet": facet, "activity": content_type, "level": "high"}]
        for segment in segments:
            sm.update(
                self._sense(
                    content_type=content_type,
                    facets=facets,
                    summary=f"{content_type} in {facet}.",
                ),
                segment,
                day,
            )
        return sm

    def _distinct_same_id_facets(self):
        return [
            {"facet": "work", "activity": "deep parser work", "level": "high"},
            {"facet": "personal", "activity": "reading docs", "level": "medium"},
        ]

    def _shared_entities(self):
        return [
            {"type": "Person", "name": "Alice", "context": "pairing"},
            {"type": "Tool", "name": "VS Code", "context": "editor"},
        ]

    def _same_id_multi_facet_machine(self, segments, day):
        from solstone.think.activity_state_machine import ActivityStateMachine

        sm = ActivityStateMachine()
        for segment in segments:
            sm.update(
                self._sense(
                    facets=self._distinct_same_id_facets(),
                    entities=self._shared_entities(),
                ),
                segment,
                day,
            )
        return sm

    def _assert_distinct_same_id_records(
        self, load_activity_records, day, expected_segments
    ):
        work_records = load_activity_records("work", day)
        personal_records = load_activity_records("personal", day)
        assert len(work_records) == 1
        assert len(personal_records) == 1

        work = work_records[0]
        personal = personal_records[0]
        assert work["description"] == "deep parser work"
        assert work["level_avg"] == 1.0
        assert work["segments"] == expected_segments
        assert work["active_entities"] == ["Alice", "VS Code"]
        assert personal["description"] == "reading docs"
        assert personal["level_avg"] == 0.5
        assert personal["segments"] == expected_segments
        assert personal["active_entities"] == ["Alice", "VS Code"]
        assert work["description"] != personal["description"]
        assert work["level_avg"] != personal["level_avg"]

    def test_multi_segment_round_trip(self, monkeypatch):
        """Multi-segment activity persists and loads with all segments intact."""
        from solstone.think.activities import (
            append_activity_record,
            load_activity_records,
        )
        from solstone.think.activity_state_machine import ActivityStateMachine

        with tempfile.TemporaryDirectory() as tmpdir:
            monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)

            sm = ActivityStateMachine()
            sm.update(self._sense(content_type="coding"), "090000_300", "20260304")
            sm.update(self._sense(content_type="coding"), "090500_300", "20260304")
            sm.update(self._sense(content_type="coding"), "091000_300", "20260304")
            # End via type change
            sm.update(self._sense(content_type="meeting"), "091500_300", "20260304")
            changes = sm.update(
                self._sense(content_type="meeting"), "092000_300", "20260304"
            )

            ended = [c for c in changes if c.get("state") == "ended"]
            assert len(ended) == 1

            # Simulate thinking.py facet_by_id logic
            facet_by_id = {
                c["id"]: c["facet"] for c in changes if c.get("state") == "ended"
            }
            for rec in sm.get_completed_activities():
                if rec["id"] in facet_by_id:
                    append_activity_record(facet_by_id[rec["id"]], "20260304", rec)

            # Verify loaded record matches
            records = load_activity_records("work", "20260304")
            assert len(records) == 1
            r = records[0]
            assert r["segments"] == [
                "090000_300",
                "090500_300",
                "091000_300",
                "091500_300",
            ]
            assert r["activity"] == "coding"
            assert isinstance(r["created_at"], int)
            assert r["created_at"] > 0

    def test_idle_ending_round_trip(self, monkeypatch):
        """Idle transition persists records correctly."""
        from solstone.think.activities import (
            append_activity_record,
            load_activity_records,
        )
        from solstone.think.activity_state_machine import ActivityStateMachine

        with tempfile.TemporaryDirectory() as tmpdir:
            monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)

            sm = ActivityStateMachine()
            sm.update(self._sense(content_type="coding"), "090000_300", "20260304")
            sm.update(self._sense(content_type="coding"), "090500_300", "20260304")
            changes = sm.update(self._sense(density="idle"), "091000_300", "20260304")

            facet_by_id = {
                c["id"]: c["facet"] for c in changes if c.get("state") == "ended"
            }
            for rec in sm.get_completed_activities():
                if rec["id"] in facet_by_id:
                    append_activity_record(facet_by_id[rec["id"]], "20260304", rec)

            records = load_activity_records("work", "20260304")
            assert len(records) == 1
            assert records[0]["segments"] == ["090000_300", "090500_300"]

    def test_deduplication_prevents_double_write(self, monkeypatch):
        """Same record written twice only appears once in the file."""
        from solstone.think.activities import (
            append_activity_record,
            load_activity_records,
        )
        from solstone.think.activity_state_machine import ActivityStateMachine

        with tempfile.TemporaryDirectory() as tmpdir:
            monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)

            sm = ActivityStateMachine()
            sm.update(self._sense(content_type="coding"), "090000_300", "20260304")
            sm.update(self._sense(content_type="meeting"), "090500_300", "20260304")
            sm.update(self._sense(content_type="meeting"), "091000_300", "20260304")

            rec = sm.get_completed_activities()[0]

            # Write once
            result1 = append_activity_record("work", "20260304", rec)
            assert result1 is True
            # Write again — should be rejected
            result2 = append_activity_record("work", "20260304", rec)
            assert result2 is False

            records = load_activity_records("work", "20260304")
            assert len(records) == 1

    def test_facet_by_id_only_persists_current_update(self, monkeypatch):
        """Only records from the current update() are persisted, not cumulative."""
        from solstone.think.activities import (
            append_activity_record,
            load_activity_records,
        )
        from solstone.think.activity_state_machine import ActivityStateMachine

        with tempfile.TemporaryDirectory() as tmpdir:
            monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)

            sm = ActivityStateMachine()
            # Activity 1 ends
            sm.update(self._sense(content_type="coding"), "090000_300", "20260304")
            sm.update(self._sense(content_type="meeting"), "090500_300", "20260304")
            changes1 = sm.update(
                self._sense(content_type="meeting"), "091000_300", "20260304"
            )
            facet_by_id = {
                c["id"]: c["facet"] for c in changes1 if c.get("state") == "ended"
            }
            for rec in sm.get_completed_activities():
                if rec["id"] in facet_by_id:
                    append_activity_record(facet_by_id[rec["id"]], "20260304", rec)

            # Activity 2 continues (no ending)
            changes2 = sm.update(
                self._sense(content_type="meeting"), "091500_300", "20260304"
            )
            # No ended changes in this update
            facet_by_id2 = {
                c["id"]: c["facet"] for c in changes2 if c.get("state") == "ended"
            }
            # get_completed_activities() still returns activity 1, but
            # facet_by_id2 is empty so nothing should be re-written
            for rec in sm.get_completed_activities():
                if rec["id"] in facet_by_id2:
                    append_activity_record(facet_by_id2[rec["id"]], "20260304", rec)

            records = load_activity_records("work", "20260304")
            assert len(records) == 1  # Only activity 1, not duplicated

    def test_jsonl_field_fidelity(self, monkeypatch):
        """All fields survive JSON serialization round-trip exactly."""
        from solstone.think.activities import (
            append_activity_record,
            load_activity_records,
        )
        from solstone.think.activity_state_machine import ActivityStateMachine

        with tempfile.TemporaryDirectory() as tmpdir:
            monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)

            entities = [
                {"type": "Person", "name": "Alice", "context": "dev"},
                {"type": "Tool", "name": "VS Code", "context": "editor"},
            ]
            sm = ActivityStateMachine()
            sm.update(
                self._sense(
                    content_type="coding",
                    entities=entities,
                    summary="Pair programming with Alice",
                ),
                "090000_300",
                "20260304",
            )
            sm.update(self._sense(content_type="meeting"), "090500_300", "20260304")
            sm.update(self._sense(content_type="meeting"), "091000_300", "20260304")

            rec = sm.get_completed_activities()[0]
            append_activity_record("work", "20260304", rec)
            loaded = load_activity_records("work", "20260304")[0]

            # Every field must round-trip exactly
            assert loaded["id"] == rec["id"]
            assert loaded["activity"] == rec["activity"]
            assert loaded["segments"] == rec["segments"]
            assert loaded["level_avg"] == rec["level_avg"]
            assert loaded["description"] == rec["description"]
            assert loaded["active_entities"] == rec["active_entities"]
            assert loaded["created_at"] == rec["created_at"]

    def test_multi_facet_ending_persists_both(self, monkeypatch):
        """Tail persistence keeps same-id sibling records facet-specific."""
        from solstone.think import thinking as think
        from solstone.think.activities import load_activity_records

        with tempfile.TemporaryDirectory() as tmpdir:
            monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)

            day = "20260304"
            sm = self._same_id_multi_facet_machine(["090000_300", "090500_300"], day)
            missing_facets = self._sense(facets=[], entities=self._shared_entities())
            think._run_activity_state_tail(
                sm,
                missing_facets,
                "091000_300",
                day,
                "segment",
                refresh=False,
                verbose=False,
                max_concurrency=1,
                skip_activity_prompts=True,
            )
            think._run_activity_state_tail(
                sm,
                missing_facets,
                "091500_300",
                day,
                "segment",
                refresh=False,
                verbose=False,
                max_concurrency=1,
                skip_activity_prompts=True,
            )

            self._assert_distinct_same_id_records(
                load_activity_records,
                day,
                ["090000_300", "090500_300", "091000_300"],
            )

    def test_close_active_flushes_dangling_activity(self, monkeypatch):
        """Dangling active state closes to one persisted clustered record."""
        from solstone.think.activities import (
            append_activity_record,
            load_activity_records,
        )

        with tempfile.TemporaryDirectory() as tmpdir:
            monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)

            segments = ["090000_300", "090500_300", "091000_300"]
            sm = self._active_machine(segments, day="20260412", facet="work")

            assert sm.get_completed_activities() == []

            changes = sm.close_active("091000_300")
            ended = [c for c in changes if c.get("state") == "ended"]
            assert len(ended) == 1
            assert ended[0]["_change"] == "ended_day_end"

            completed_lookup = {}
            for rec in sm.get_completed_activities():
                completed_lookup.setdefault(rec["id"], rec)
            for change in ended:
                append_activity_record(
                    change["facet"],
                    "20260412",
                    completed_lookup[change["id"]],
                )

            records = load_activity_records("work", "20260412")
            assert len(records) == 1
            assert records[0]["segments"] == segments

    def test_flush_import_day_clusters_one_record(self, monkeypatch):
        """Import stream batch flush writes one clustered dangling record."""
        from solstone.think.activities import load_activity_records
        from solstone.think.thinking import _flush_batch_state_machines

        with tempfile.TemporaryDirectory() as tmpdir:
            monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)

            segments = ["162416_300", "162916_300", "163416_300"]
            sm = self._active_machine(segments, day="20260412", facet="solstone")
            _flush_batch_state_machines(
                {"import.audio": sm},
                "20260412",
                refresh=False,
                verbose=False,
                max_concurrency=1,
                skip_activity_prompts=True,
            )

            records = load_activity_records("solstone", "20260412")
            assert len(records) == 1
            assert records[0]["segments"] == segments

    def test_flush_same_id_siblings_persist_independent_records(self, monkeypatch):
        """Batch flush writes each same-id sibling's own completed record."""
        from solstone.think.activities import load_activity_records
        from solstone.think.thinking import _flush_batch_state_machines

        with tempfile.TemporaryDirectory() as tmpdir:
            monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)

            day = "20260412"
            segments = ["162416_300", "162916_300"]
            for _ in range(2):
                sm = self._same_id_multi_facet_machine(segments, day)
                _flush_batch_state_machines(
                    {"import.audio": sm},
                    day,
                    refresh=False,
                    verbose=False,
                    max_concurrency=1,
                    skip_activity_prompts=True,
                )

            self._assert_distinct_same_id_records(
                load_activity_records,
                day,
                segments,
            )

    def test_flush_is_idempotent(self, monkeypatch):
        """Equivalent batch flushes for the same import day dedupe by id."""
        from solstone.think.activities import load_activity_records
        from solstone.think.thinking import _flush_batch_state_machines

        with tempfile.TemporaryDirectory() as tmpdir:
            monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)

            segments = ["162416_300", "162916_300"]
            for _ in range(2):
                sm = self._active_machine(segments, day="20260412", facet="solstone")
                _flush_batch_state_machines(
                    {"import.audio": sm},
                    "20260412",
                    refresh=False,
                    verbose=False,
                    max_concurrency=1,
                    skip_activity_prompts=True,
                )

            records = load_activity_records("solstone", "20260412")
            assert len(records) == 1
            assert records[0]["segments"] == segments

    def test_flush_cross_stream_no_merge(self, monkeypatch):
        """Per-stream machines keep same-facet streams as separate records."""
        from solstone.think.activities import load_activity_records
        from solstone.think.thinking import _flush_batch_state_machines

        with tempfile.TemporaryDirectory() as tmpdir:
            monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)

            observer_segments = ["090000_300", "090500_300"]
            import_segments = ["090200_300", "090700_300"]
            observer_sm = self._active_machine(
                observer_segments,
                day="20260412",
                content_type="meeting",
                facet="work",
            )
            import_sm = self._active_machine(
                import_segments,
                day="20260412",
                content_type="meeting",
                facet="work",
            )

            _flush_batch_state_machines(
                {"archon": observer_sm, "import.audio": import_sm},
                "20260412",
                refresh=False,
                verbose=False,
                max_concurrency=1,
                skip_activity_prompts=True,
            )

            records = load_activity_records("work", "20260412")
            assert len(records) == 2
            by_id = {record["id"]: record for record in records}
            assert by_id["meeting_090000_300"]["segments"] == observer_segments
            assert by_id["meeting_090200_300"]["segments"] == import_segments

    def test_flush_observer_active_on_today_not_flushed(self, monkeypatch):
        """Current-day observer streams are not truncated by batch flush."""
        from datetime import datetime

        from solstone.think.activities import load_activity_records
        from solstone.think.thinking import _flush_batch_state_machines

        with tempfile.TemporaryDirectory() as tmpdir:
            monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)

            day = datetime.now().strftime("%Y%m%d")
            sm = self._active_machine(["090000_300", "090500_300"], day=day)

            _flush_batch_state_machines(
                {"archon": sm},
                day,
                refresh=False,
                verbose=False,
                max_concurrency=1,
                skip_activity_prompts=True,
            )

            assert load_activity_records("work", day) == []

    def test_flush_observer_past_day_is_flushed(self, monkeypatch):
        """Past observer streams are closed at day end."""
        from datetime import datetime, timedelta

        from solstone.think.activities import load_activity_records
        from solstone.think.thinking import _flush_batch_state_machines

        with tempfile.TemporaryDirectory() as tmpdir:
            monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)

            day = (datetime.now() - timedelta(days=1)).strftime("%Y%m%d")
            segments = ["090000_300", "090500_300"]
            sm = self._active_machine(segments, day=day)

            _flush_batch_state_machines(
                {"archon": sm},
                day,
                refresh=False,
                verbose=False,
                max_concurrency=1,
                skip_activity_prompts=True,
            )

            records = load_activity_records("work", day)
            assert len(records) == 1
            assert records[0]["segments"] == segments


class TestCreatedAtRoutesCompat:
    """Verify created_at works with the routes.py comparison and display logic."""

    def test_created_at_passes_cutoff_filter(self, monkeypatch):
        """Simulate routes.py filtering — recent records pass, old records don't."""
        from datetime import datetime, timedelta

        from solstone.think.activities import (
            append_activity_record,
            load_activity_records,
        )
        from solstone.think.activity_state_machine import ActivityStateMachine

        with tempfile.TemporaryDirectory() as tmpdir:
            monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)

            sm = ActivityStateMachine()
            sm.update(
                {
                    "density": "active",
                    "content_type": "coding",
                    "activity_summary": "test",
                    "entities": [],
                    "facets": [
                        {"facet": "work", "activity": "coding", "level": "high"}
                    ],
                    "meeting_detected": False,
                    "speakers": [],
                    "recommend": {},
                },
                "090000_300",
                "20260304",
            )
            sm.update(
                {
                    "density": "active",
                    "content_type": "meeting",
                    "activity_summary": "standup",
                    "entities": [],
                    "facets": [
                        {"facet": "work", "activity": "meeting", "level": "medium"}
                    ],
                    "meeting_detected": True,
                    "speakers": [],
                    "recommend": {},
                },
                "090500_300",
                "20260304",
            )
            sm.update(
                {
                    "density": "active",
                    "content_type": "meeting",
                    "activity_summary": "standup",
                    "entities": [],
                    "facets": [
                        {"facet": "work", "activity": "meeting", "level": "medium"}
                    ],
                    "meeting_detected": True,
                    "speakers": [],
                    "recommend": {},
                },
                "091000_300",
                "20260304",
            )
            rec = sm.get_completed_activities()[0]
            append_activity_record("work", "20260304", rec)

            records = load_activity_records("work", "20260304")
            record = records[0]

            # Simulate routes.py line 273 and 279
            now = datetime.now()
            cutoff_ts = (now - timedelta(hours=4)).timestamp() * 1000
            created_at = record.get("created_at", 0)

            # This comparison must not raise TypeError
            recent = created_at >= cutoff_ts
            assert recent is True  # Just created, must be recent

            # Simulate routes.py line 283-284
            dt = datetime.fromtimestamp(created_at / 1000)
            display_time = dt.strftime("%H:%M")
            assert len(display_time) == 5  # "HH:MM"

    def test_sort_by_created_at(self, monkeypatch):
        """Multiple records can be sorted by created_at (routes.py line 290)."""
        import time

        from solstone.think.activities import (
            append_activity_record,
            load_activity_records,
        )
        from solstone.think.activity_state_machine import ActivityStateMachine

        with tempfile.TemporaryDirectory() as tmpdir:
            monkeypatch.setenv("SOLSTONE_JOURNAL", tmpdir)

            sm = ActivityStateMachine()
            sm.update(
                {
                    "density": "active",
                    "content_type": "coding",
                    "activity_summary": "first",
                    "entities": [],
                    "facets": [
                        {"facet": "work", "activity": "coding", "level": "high"}
                    ],
                    "meeting_detected": False,
                    "speakers": [],
                    "recommend": {},
                },
                "090000_300",
                "20260304",
            )
            sm.update(
                {
                    "density": "active",
                    "content_type": "meeting",
                    "activity_summary": "second",
                    "entities": [],
                    "facets": [
                        {"facet": "work", "activity": "meeting", "level": "medium"}
                    ],
                    "meeting_detected": True,
                    "speakers": [],
                    "recommend": {},
                },
                "090500_300",
                "20260304",
            )
            changes1 = sm.update(
                {
                    "density": "active",
                    "content_type": "meeting",
                    "activity_summary": "second",
                    "entities": [],
                    "facets": [
                        {"facet": "work", "activity": "meeting", "level": "medium"}
                    ],
                    "meeting_detected": True,
                    "speakers": [],
                    "recommend": {},
                },
                "091000_300",
                "20260304",
            )
            # Persist first completed
            facet_by_id = {
                c["id"]: c["facet"] for c in changes1 if c.get("state") == "ended"
            }
            for rec in sm.get_completed_activities():
                if rec["id"] in facet_by_id:
                    append_activity_record(facet_by_id[rec["id"]], "20260304", rec)

            # Small delay so created_at differs
            time.sleep(0.01)

            sm.update(
                {
                    "density": "active",
                    "content_type": "coding",
                    "activity_summary": "third",
                    "entities": [],
                    "facets": [
                        {"facet": "work", "activity": "coding", "level": "high"}
                    ],
                    "meeting_detected": False,
                    "speakers": [],
                    "recommend": {},
                },
                "091000_300",
                "20260304",
            )
            changes2 = sm.update(
                {
                    "density": "active",
                    "content_type": "coding",
                    "activity_summary": "third",
                    "entities": [],
                    "facets": [
                        {"facet": "work", "activity": "coding", "level": "high"}
                    ],
                    "meeting_detected": False,
                    "speakers": [],
                    "recommend": {},
                },
                "092000_300",
                "20260304",
            )
            facet_by_id2 = {
                c["id"]: c["facet"] for c in changes2 if c.get("state") == "ended"
            }
            for rec in sm.get_completed_activities():
                if rec["id"] in facet_by_id2:
                    append_activity_record(facet_by_id2[rec["id"]], "20260304", rec)

            records = load_activity_records("work", "20260304")
            assert len(records) == 2
            # Simulate routes.py sort
            sorted_records = sorted(
                records, key=lambda a: a.get("created_at", 0), reverse=True
            )
            assert sorted_records[0]["created_at"] >= sorted_records[1]["created_at"]


# ---------------------------------------------------------------------------
# Activity template variables in agents.py
# ---------------------------------------------------------------------------


class TestActivityTemplateVars:
    """Tests for activity template variables in _build_prompt_context."""

    def test_activity_vars_populated(self):
        from solstone.think.talents import _build_prompt_context

        activity = {
            "id": "coding_100000_300",
            "activity": "coding",
            "description": "Extended coding session with Git and VS Code",
            "level_avg": 0.83,
            "active_entities": ["VS Code", "Git"],
            "segments": ["100000_300", "100500_300"],
        }

        ctx = _build_prompt_context(
            "20260209", None, ["100000_300", "100500_300"], activity=activity
        )

        assert ctx["activity_id"] == "coding_100000_300"
        assert ctx["activity_type"] == "coding"
        assert (
            ctx["activity_description"]
            == "Extended coding session with Git and VS Code"
        )
        assert ctx["activity_level"] == "0.83"
        assert ctx["activity_entities"] == "VS Code, Git"
        assert ctx["activity_segments"] == "100000_300, 100500_300"
        assert int(ctx["activity_duration"]) == 10  # 2 * 300s = 10 min

    def test_no_activity_no_vars(self):
        from solstone.think.talents import _build_prompt_context

        ctx = _build_prompt_context("20260209", None, None)

        assert "activity_id" not in ctx
        assert "activity_type" not in ctx

    def test_empty_entities(self):
        from solstone.think.talents import _build_prompt_context

        activity = {
            "id": "browsing_100000_300",
            "activity": "browsing",
            "description": "Browsing",
            "level_avg": 0.5,
            "active_entities": [],
            "segments": ["100000_300"],
        }

        ctx = _build_prompt_context("20260209", None, ["100000_300"], activity=activity)

        assert ctx["activity_entities"] == ""

    def test_duration_minimum_one(self):
        from solstone.think.talents import _build_prompt_context

        activity = {
            "id": "test_bad_seg",
            "activity": "test",
            "description": "",
            "level_avg": 0.5,
            "active_entities": [],
            "segments": ["invalid"],
        }

        ctx = _build_prompt_context("20260209", None, None, activity=activity)

        # Invalid segment can't parse, should default to 1
        assert ctx["activity_duration"] == "1"

    def test_facet_and_activity_md_dir_populated(self, monkeypatch, tmp_path):
        from solstone.think.talents import _build_prompt_context

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

        ctx = _build_prompt_context(
            day="20260418",
            segment=None,
            span=None,
            facet="work",
        )

        assert ctx["facet"] == "work"
        assert ctx["activity_md_dir"] == f"{tmp_path}/facets/work/activities/20260418/"

    def test_activity_md_dir_omitted_without_facet(self):
        from solstone.think.talents import _build_prompt_context

        ctx = _build_prompt_context(day="20260418", segment=None, span=None)

        assert "facet" not in ctx
        assert "activity_md_dir" not in ctx


def test_prepare_config_substitutes_facet_and_activity_md_dir_for_daily_cogitate(
    tmp_path, monkeypatch
):
    import importlib

    import solstone.think.talent as talent
    from solstone.think.utils import day_path

    mod = importlib.import_module("solstone.think.talents")

    monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
    day_dir = day_path("20260418")
    day_dir.mkdir(parents=True, exist_ok=True)

    monkeypatch.setattr(talent, "TALENT_DIR", tmp_path)

    test_agent = tmp_path / "test_activities_cogitate.md"
    test_agent.write_text(
        "{\n"
        '  "type": "cogitate",\n'
        '  "schedule": "daily",\n'
        '  "priority": 30,\n'
        '  "multi_facet": true\n'
        "}\n\n"
        "Facet: $facet\n"
        "Dir: $activity_md_dir\n"
    )

    monkeypatch.setenv("GOOGLE_API_KEY", "x")
    config_dir = tmp_path / "config"
    config_dir.mkdir()
    (config_dir / "journal.json").write_text(
        json.dumps(
            {
                "providers": {
                    "active": {
                        "provider": "google",
                        "model": "gemini-3.5-flash",
                    }
                }
            }
        ),
        encoding="utf-8",
    )

    config = mod.prepare_config(
        {
            "name": "test_activities_cogitate",
            "day": "20260418",
            "facet": "work",
        }
    )

    assert "Facet: work" in config["user_instruction"]
    assert "facets/work/activities/20260418/" in config["user_instruction"]
    assert "$facet" not in config["user_instruction"]
    assert "$activity_md_dir" not in config["user_instruction"]


# ---------------------------------------------------------------------------
# Talent config validation for activity schedule
# ---------------------------------------------------------------------------


class TestTalentActivityValidation:
    """Tests for activity schedule validation in get_talent_configs."""

    def _isolate_talent(self, monkeypatch, tmp_path):
        """Point talent discovery at tmp_path only (no real talent/ or apps/)."""
        talent_dir = tmp_path / "talent"
        talent_dir.mkdir(exist_ok=True)
        monkeypatch.setattr("solstone.think.talent.TALENT_DIR", talent_dir)
        monkeypatch.setattr("solstone.think.talent.APPS_DIR", tmp_path / "no_apps")
        return talent_dir

    def test_missing_activities_field_raises(self, monkeypatch, tmp_path):
        import frontmatter

        from solstone.think.talent import get_talent_configs

        talent_dir = self._isolate_talent(monkeypatch, tmp_path)

        post = frontmatter.Post(
            "Test prompt",
            schedule="activity",
            priority=50,
            type="generate",
            output="md",
            # Missing 'activities' field
        )
        (talent_dir / "test_agent.md").write_text(frontmatter.dumps(post))

        with pytest.raises(ValueError, match="non-empty 'activities' list"):
            get_talent_configs(schedule="activity")

    def test_valid_activities_field_passes(self, monkeypatch, tmp_path):
        import frontmatter

        from solstone.think.talent import get_talent_configs

        talent_dir = self._isolate_talent(monkeypatch, tmp_path)

        post = frontmatter.Post(
            "Test prompt",
            schedule="activity",
            priority=50,
            type="generate",
            output="md",
            activities=["coding", "meeting"],
        )
        (talent_dir / "test_agent.md").write_text(frontmatter.dumps(post))

        configs = get_talent_configs(schedule="activity")
        assert "test_agent" in configs
        assert configs["test_agent"]["activities"] == ["coding", "meeting"]

    def test_wildcard_activities_passes(self, monkeypatch, tmp_path):
        import frontmatter

        from solstone.think.talent import get_talent_configs

        talent_dir = self._isolate_talent(monkeypatch, tmp_path)

        post = frontmatter.Post(
            "Test prompt",
            schedule="activity",
            priority=50,
            type="generate",
            output="md",
            activities=["*"],
        )
        (talent_dir / "test_agent.md").write_text(frontmatter.dumps(post))

        configs = get_talent_configs(schedule="activity")
        assert "test_agent" in configs
        assert configs["test_agent"]["activities"] == ["*"]


# ---------------------------------------------------------------------------
# CLI argument validation
# ---------------------------------------------------------------------------


class TestActivityCLIArgs:
    """Tests for think CLI argument validation."""

    def test_activity_requires_facet(self, monkeypatch):
        from solstone.think.thinking import parse_args

        parser = parse_args()

        monkeypatch.setattr(
            "sys.argv",
            ["sol think", "--activity", "coding_100000_300", "--day", "20260209"],
        )

        # parse_args returns the parser, not args — need to test via main()
        # Instead test the parser directly
        args = parser.parse_args(
            ["--activity", "coding_100000_300", "--day", "20260209"]
        )
        assert args.activity == "coding_100000_300"
        assert args.facet is None  # Validation happens in main()

    def test_activity_args_parsed(self):
        from solstone.think.thinking import parse_args

        parser = parse_args()
        args = parser.parse_args(
            [
                "--activity",
                "coding_100000_300",
                "--facet",
                "work",
                "--day",
                "20260209",
            ]
        )
        assert args.activity == "coding_100000_300"
        assert args.facet == "work"
        assert args.day == "20260209"
