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

"""Tests for the deterministic activity state machine."""


def _sense(
    content_type="coding",
    density="active",
    facets=None,
    summary="Working on code.",
    facet_activity="Reviewing the open diff.",
    entities=None,
    meeting=False,
    speakers=None,
):
    """Build a Sense output payload for testing."""
    if facets is None:
        facets = [{"facet": "work", "activity": facet_activity, "level": "high"}]
    return {
        "density": density,
        "content_type": content_type,
        "activity_summary": summary,
        "entities": entities or [],
        "facets": facets,
        "meeting_detected": meeting,
        "speakers": speakers or [],
        "recommend": {},
    }


class TestNewActivity:
    def test_first_segment_starts_new(self):
        from solstone.think.activity_state_machine import ActivityStateMachine

        sm = ActivityStateMachine()
        changes = sm.update(_sense(), "090000_300", "20260304")

        assert len(changes) == 1
        assert changes[0]["_change"] == "new"
        assert changes[0]["activity"] == "coding"
        assert changes[0]["state"] == "active"
        assert changes[0]["since"] == "090000_300"
        assert changes[0]["facet"] == "work"


class TestContinuation:
    def test_same_type_continues(self):
        from solstone.think.activity_state_machine import ActivityStateMachine

        sm = ActivityStateMachine()
        sm.update(_sense(), "090000_300", "20260304")
        changes = sm.update(
            _sense(facet_activity="Still reviewing the diff."),
            "090500_300",
            "20260304",
        )

        assert len(changes) == 1
        assert changes[0]["_change"] == "continuing"
        assert changes[0]["since"] == "090000_300"
        assert changes[0]["description"] == "Still reviewing the diff."


class TestContentTypeChange:
    def test_type_change_ends_old_starts_new(self):
        from solstone.think.activity_state_machine import ActivityStateMachine

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

        assert all(c["state"] != "ended" for c in pending)
        assert pending[0]["_change"] == "type_change_pending"
        assert len(changes) == 2
        ended = [c for c in changes if c["state"] == "ended"]
        started = [c for c in changes if c["state"] == "active"]
        assert len(ended) == 1
        assert ended[0]["_change"] == "ended_type_change"
        assert ended[0]["activity"] == "coding"
        assert len(started) == 1
        assert started[0]["_change"] == "new"
        assert started[0]["activity"] == "meeting"


class TestIdleTransition:
    def test_idle_ends_all(self):
        from solstone.think.activity_state_machine import ActivityStateMachine

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

        ended = [c for c in changes if c["state"] == "ended"]
        assert len(ended) == 1
        assert ended[0]["_change"] == "ended_idle"
        assert sm.state == {}


class TestTimeGap:
    def test_gap_over_600s_resets(self):
        from solstone.think.activity_state_machine import ActivityStateMachine

        sm = ActivityStateMachine()
        sm.update(_sense(), "090000_300", "20260304")
        changes = sm.update(_sense(), "100600_300", "20260304")

        ended_gap = [c for c in changes if c["_change"] == "ended_gap"]
        assert len(ended_gap) == 1
        new = [c for c in changes if c["_change"] == "new"]
        assert len(new) == 1

    def test_gap_equal_600s_no_reset(self):
        from solstone.think.activity_state_machine import ActivityStateMachine

        sm = ActivityStateMachine()
        sm.update(_sense(), "090000_300", "20260304")
        changes = sm.update(_sense(), "091500_300", "20260304")

        assert all(c["_change"] != "ended_gap" for c in changes)


class TestDayBoundary:
    def test_day_change_resets(self):
        from solstone.think.activity_state_machine import ActivityStateMachine

        sm = ActivityStateMachine()
        sm.update(_sense(), "230000_300", "20260304")
        changes = sm.update(_sense(), "000000_300", "20260305")

        ended_gap = [c for c in changes if c["_change"] == "ended_gap"]
        assert len(ended_gap) == 1


class TestMultiFacet:
    def test_independent_facet_tracking(self):
        from solstone.think.activity_state_machine import ActivityStateMachine

        sm = ActivityStateMachine()
        facets = [
            {"facet": "work", "activity": "coding", "level": "high"},
            {"facet": "personal", "activity": "browsing", "level": "medium"},
        ]
        changes = sm.update(_sense(facets=facets), "090000_300", "20260304")

        assert len(changes) == 2
        facet_names = {c["facet"] for c in changes}
        assert facet_names == {"work", "personal"}


class TestLevelGate:
    def test_explicit_low_facets_are_excluded_from_active_set(self):
        from solstone.think.activity_state_machine import ActivityStateMachine

        sm = ActivityStateMachine()
        facets = [
            {"facet": "solpbc", "activity": "Planning launch work.", "level": "high"},
            {
                "facet": "personal",
                "activity": "Coordinating a personal errand.",
                "level": "medium",
            },
            {
                "facet": "bluesky",
                "activity": "Checking a social thread.",
                "level": "low",
            },
            {
                "facet": "kognova",
                "activity": "Skimming background notes.",
                "level": "low",
            },
        ]

        changes = sm.update(
            _sense(facets=facets, summary="Mixed activity summary."),
            "090000_300",
            "20260304",
        )

        assert {change["facet"] for change in changes} == {"solpbc", "personal"}
        assert set(sm.state) == {"solpbc", "personal"}
        assert not any(
            change.get("facet") in {"bluesky", "kognova"} for change in changes
        )

    def test_description_uses_each_engaged_facets_activity(self):
        from solstone.think.activity_state_machine import ActivityStateMachine

        sm = ActivityStateMachine()
        summary = "Segment-level summary for multiple domains."
        facets = [
            {
                "facet": "solpbc",
                "activity": "Reviewing release blockers.",
                "level": "high",
            },
            {
                "facet": "personal",
                "activity": "Sorting out afternoon plans.",
                "level": "medium",
            },
        ]

        changes = sm.update(
            _sense(facets=facets, summary=summary),
            "090000_300",
            "20260304",
        )

        by_facet = {change["facet"]: change for change in changes}
        assert by_facet["solpbc"]["description"] == "Reviewing release blockers."
        assert by_facet["personal"]["description"] == "Sorting out afternoon plans."
        assert all(change["description"] != summary for change in changes)

    def test_description_falls_back_to_summary_when_facet_activity_missing(self):
        from solstone.think.activity_state_machine import ActivityStateMachine

        sm = ActivityStateMachine()
        summary = "Segment summary fills missing facet activity."
        facets = [
            {"facet": "work", "activity": "", "level": "high"},
            {"facet": "personal", "level": "medium"},
        ]

        changes = sm.update(
            _sense(facets=facets, summary=summary),
            "090000_300",
            "20260304",
        )

        assert {change["facet"] for change in changes} == {"work", "personal"}
        assert all(change["description"] == summary for change in changes)
        assert all(change["description"] for change in changes)

    def test_one_low_segment_does_not_end_prior_high_facet(self):
        from solstone.think.activity_state_machine import ActivityStateMachine

        sm = ActivityStateMachine()
        high = [{"facet": "work", "activity": "Writing the parser.", "level": "high"}]
        low = [{"facet": "work", "activity": "Barely checking parser.", "level": "low"}]

        sm.update(_sense(facets=high), "090000_300", "20260304")
        pending = sm.update(_sense(facets=low), "090500_300", "20260304")

        assert len(pending) == 1
        assert pending[0]["_change"] == "facet_gone_pending"
        assert pending[0]["facet"] == "work"
        assert sm.state["work"]["_pending_facet_misses"] == 1
        assert not any(
            change.get("_change") == "ended_facet_gone" for change in pending
        )

        returned = sm.update(_sense(facets=high), "091000_300", "20260304")

        assert len(returned) == 1
        assert returned[0]["_change"] == "continuing"
        assert returned[0]["since"] == "090000_300"
        assert sm.state["work"]["_pending_facet_misses"] == 0
        assert sm.get_completed_activities() == []

    def test_two_low_segments_end_prior_high_facet(self):
        from solstone.think.activity_state_machine import ActivityStateMachine

        sm = ActivityStateMachine()
        high = [{"facet": "work", "activity": "Writing the parser.", "level": "high"}]
        low = [{"facet": "work", "activity": "Barely checking parser.", "level": "low"}]

        sm.update(_sense(facets=high), "090000_300", "20260304")
        sm.update(_sense(facets=low), "090500_300", "20260304")
        changes = sm.update(_sense(facets=low), "091000_300", "20260304")

        ended = [
            change for change in changes if change.get("_change") == "ended_facet_gone"
        ]
        assert len(ended) == 1
        assert ended[0]["state"] == "ended"
        assert ended[0]["facet"] == "work"
        assert sm.state == {}
        assert len(sm.get_completed_activities()) == 1

    def test_all_low_fresh_segment_creates_no_activity(self):
        from solstone.think.activity_state_machine import ActivityStateMachine

        sm = ActivityStateMachine()
        facets = [
            {"facet": "work", "activity": "Light inbox scan.", "level": "low"},
            {"facet": "personal", "activity": "Background reading.", "level": "low"},
        ]

        changes = sm.update(
            _sense(facets=facets, summary="Low-attention activity."),
            "090000_300",
            "20260304",
        )

        assert changes == []
        assert sm.state == {}
        assert sm.get_completed_activities() == []

    def test_all_low_segments_wind_down_prior_active_facet(self):
        from solstone.think.activity_state_machine import ActivityStateMachine

        sm = ActivityStateMachine()
        high = [
            {"facet": "work", "activity": "Implementing the parser.", "level": "high"}
        ]
        all_low = [
            {"facet": "work", "activity": "Glancing at parser logs.", "level": "low"},
            {
                "facet": "personal",
                "activity": "Skimming personal email.",
                "level": "low",
            },
        ]

        sm.update(_sense(facets=high), "090000_300", "20260304")
        pending = sm.update(_sense(facets=all_low), "090500_300", "20260304")
        ended_changes = sm.update(_sense(facets=all_low), "091000_300", "20260304")

        assert len(pending) == 1
        assert pending[0]["_change"] == "facet_gone_pending"
        assert pending[0]["_pending_facet_misses"] == 1
        ended = [
            change
            for change in ended_changes
            if change.get("_change") == "ended_facet_gone"
        ]
        assert len(ended) == 1
        assert ended[0]["facet"] == "work"
        assert sm.state == {}


class TestFacetDisappearing:
    def test_facet_gone_emits_ended(self):
        from solstone.think.activity_state_machine import ActivityStateMachine

        sm = ActivityStateMachine()
        two_facets = [
            {"facet": "work", "activity": "coding", "level": "high"},
            {"facet": "personal", "activity": "browsing", "level": "medium"},
        ]
        sm.update(_sense(facets=two_facets), "090000_300", "20260304")
        one_facet = [{"facet": "work", "activity": "coding", "level": "high"}]
        pending = sm.update(_sense(facets=one_facet), "090500_300", "20260304")
        changes = sm.update(_sense(facets=one_facet), "091000_300", "20260304")

        assert pending[0]["_change"] == "facet_gone_pending"
        ended = [c for c in changes if c["_change"] == "ended_facet_gone"]
        assert len(ended) == 1
        assert ended[0]["facet"] == "personal"


class TestStateShape:
    def test_active_entries_use_persisted_field_names(self):
        from solstone.think.activity_state_machine import ActivityStateMachine

        sm = ActivityStateMachine()
        sm.update(_sense(), "090000_300", "20260304")

        assert set(sm.state) == {"work"}
        entry = sm.state["work"]
        assert "id" in entry
        assert "activity" in entry
        assert "state" in entry and entry["state"] == "active"
        assert "since" in entry
        assert "level" in entry
        assert "active_entities" in entry
        assert entry["facet"] == "work"
        assert entry["segment"] == "090000_300"
        assert entry["segments"] == ["090000_300"]
        assert entry["_change"] == "new"
        assert "_facet" not in entry
        assert "_segment" not in entry
        assert "_segments" not in entry


class TestGetCompletedActivities:
    def test_completed_format(self):
        from solstone.think.activity_state_machine import ActivityStateMachine

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

        assert len(completed) == 1
        rec = completed[0]
        assert "id" in rec
        assert "activity" in rec and rec["activity"] == "coding"
        assert "segments" in rec and isinstance(rec["segments"], list)
        assert rec["segments"] == ["090000_300", "090500_300"]
        assert "level_avg" in rec and isinstance(rec["level_avg"], float)
        assert "description" in rec
        assert "active_entities" in rec
        assert "created_at" in rec
        assert isinstance(rec["created_at"], int)


class TestSegmentAccumulation:
    def test_continuing_accumulates_segments(self):
        from solstone.think.activity_state_machine import ActivityStateMachine

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

        completed = sm.get_completed_activities()
        assert len(completed) == 1
        rec = completed[0]
        assert rec["segments"] == [
            "090000_300",
            "090500_300",
            "091000_300",
            "091500_300",
        ]

    def test_ten_segments_produces_ten_keys(self):
        from solstone.think.activity_state_machine import ActivityStateMachine

        sm = ActivityStateMachine()
        for i in range(10):
            minutes = i * 5
            seg = f"09{minutes:02d}00_300"
            sm.update(_sense(content_type="coding"), seg, "20260304")
        # End with idle
        sm.update(_sense(density="idle"), "095000_300", "20260304")

        completed = sm.get_completed_activities()
        assert len(completed) == 1
        assert len(completed[0]["segments"]) == 10

    def test_segments_accumulate_in_state(self):
        from solstone.think.activity_state_machine import ActivityStateMachine

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

        assert sm.state["work"]["segments"] == ["090000_300", "090500_300"]


class TestEntityTracking:
    def test_extracts_names(self):
        from solstone.think.activity_state_machine import ActivityStateMachine

        sm = ActivityStateMachine()
        entities = [
            {"type": "Person", "name": "Alice", "context": "colleague"},
            {"type": "Tool", "name": "VSCode", "context": "editor"},
        ]
        changes = sm.update(_sense(entities=entities), "090000_300", "20260304")

        assert changes[0]["active_entities"] == ["Alice", "VSCode"]

    def test_skips_blank_names(self):
        from solstone.think.activity_state_machine import ActivityStateMachine

        sm = ActivityStateMachine()
        entities = [
            {"type": "Person", "name": "", "context": "unknown"},
            {"type": "Tool", "context": "no name key"},
        ]
        changes = sm.update(_sense(entities=entities), "090000_300", "20260304")

        assert changes[0]["active_entities"] == []


class TestSegmentAccumulationEdgeCases:
    """Regression tests for segment accumulation across all ending paths."""

    def test_idle_ending_preserves_accumulated_segments(self):
        """Idle transition after multi-segment activity must carry all segments."""
        from solstone.think.activity_state_machine import ActivityStateMachine

        sm = ActivityStateMachine()
        sm.update(_sense(content_type="coding"), "090000_300", "20260304")
        sm.update(_sense(content_type="coding"), "090500_300", "20260304")
        sm.update(_sense(content_type="coding"), "091000_300", "20260304")
        # End via idle
        sm.update(_sense(density="idle"), "091500_300", "20260304")

        completed = sm.get_completed_activities()
        assert len(completed) == 1
        assert completed[0]["segments"] == [
            "090000_300",
            "090500_300",
            "091000_300",
        ]

    def test_facet_gone_preserves_accumulated_segments(self):
        """Facet disappearing after multi-segment activity must carry all segments.

        Note: the state machine uses the top-level content_type for all facets,
        not the per-facet activity field. So both facets have activity=="coding".
        """
        from solstone.think.activity_state_machine import ActivityStateMachine

        sm = ActivityStateMachine()
        two = [
            {"facet": "work", "activity": "coding", "level": "high"},
            {"facet": "personal", "activity": "browsing", "level": "medium"},
        ]
        one = [{"facet": "work", "activity": "coding", "level": "high"}]
        sm.update(_sense(facets=two), "090000_300", "20260304")
        sm.update(_sense(facets=two), "090500_300", "20260304")
        sm.update(_sense(facets=two), "091000_300", "20260304")
        # personal disappears
        sm.update(_sense(facets=one), "091500_300", "20260304")
        sm.update(_sense(facets=one), "092000_300", "20260304")

        completed = sm.get_completed_activities()
        assert len(completed) == 1
        # Activity type comes from top-level content_type, not per-facet
        assert completed[0]["activity"] == "coding"
        assert completed[0]["segments"] == [
            "090000_300",
            "090500_300",
            "091000_300",
            "091500_300",
        ]

    def test_gap_ending_preserves_accumulated_segments(self):
        """Time gap after multi-segment activity must carry all segments."""
        from solstone.think.activity_state_machine import ActivityStateMachine

        sm = ActivityStateMachine()
        sm.update(_sense(content_type="coding"), "090000_300", "20260304")
        sm.update(_sense(content_type="coding"), "090500_300", "20260304")
        # 20-minute gap triggers reset (> 600s threshold)
        sm.update(_sense(content_type="coding"), "093000_300", "20260304")

        completed = sm.get_completed_activities()
        assert len(completed) == 1
        assert completed[0]["segments"] == ["090000_300", "090500_300"]

    def test_type_change_preserves_accumulated_segments(self):
        """Type change after multi-segment activity must carry all segments."""
        from solstone.think.activity_state_machine import ActivityStateMachine

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

        completed = sm.get_completed_activities()
        assert len(completed) == 1
        assert completed[0]["segments"] == [
            "090000_300",
            "090500_300",
            "091000_300",
            "091500_300",
            "092000_300",
        ]

    def test_single_segment_activity_has_one_segment(self):
        """Activity lasting exactly one segment has segments: [that_segment]."""
        from solstone.think.activity_state_machine import ActivityStateMachine

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

        completed = sm.get_completed_activities()
        assert len(completed) == 1
        assert completed[0]["segments"] == ["090000_300"]

    def test_duplicate_segment_key_not_appended(self):
        """Same segment key processed twice doesn't duplicate in _segments."""
        from solstone.think.activity_state_machine import ActivityStateMachine

        sm = ActivityStateMachine()
        sm.update(_sense(content_type="coding"), "090000_300", "20260304")
        # Same segment key again (shouldn't happen in practice, but defensive)
        sm.update(_sense(content_type="coding"), "090000_300", "20260304")
        sm.update(_sense(content_type="meeting"), "090500_300", "20260304")
        sm.update(_sense(content_type="meeting"), "091000_300", "20260304")

        completed = sm.get_completed_activities()
        assert len(completed) == 1
        assert completed[0]["segments"] == ["090000_300", "090500_300"]

    def test_multi_facet_simultaneous_ending_all_have_segments(self):
        """Multiple facets ending simultaneously each have their own segments."""
        from solstone.think.activity_state_machine import ActivityStateMachine

        sm = ActivityStateMachine()
        two = [
            {"facet": "work", "activity": "coding", "level": "high"},
            {"facet": "personal", "activity": "browsing", "level": "medium"},
        ]
        sm.update(_sense(facets=two), "090000_300", "20260304")
        sm.update(_sense(facets=two), "090500_300", "20260304")
        sm.update(_sense(facets=two), "091000_300", "20260304")
        # Both end via idle
        sm.update(_sense(density="idle"), "091500_300", "20260304")

        completed = sm.get_completed_activities()
        assert len(completed) == 2
        for rec in completed:
            assert rec["segments"] == [
                "090000_300",
                "090500_300",
                "091000_300",
            ]


class TestCompletedRecordFields:
    """Verify all fields in completed records have correct types and values."""

    def test_created_at_is_millisecond_int(self):
        """created_at must be an integer in milliseconds (not ISO string)."""
        import time

        from solstone.think.activity_state_machine import ActivityStateMachine

        before = int(time.time() * 1000)
        sm = ActivityStateMachine()
        sm.update(_sense(content_type="coding"), "090000_300", "20260304")
        sm.update(_sense(content_type="meeting"), "090500_300", "20260304")
        sm.update(_sense(content_type="meeting"), "091000_300", "20260304")
        after = int(time.time() * 1000)

        rec = sm.get_completed_activities()[0]
        assert isinstance(rec["created_at"], int)
        assert before <= rec["created_at"] <= after

    def test_created_at_comparable_to_float_cutoff(self):
        """created_at must be comparable with float (routes.py cutoff_ts)."""
        from datetime import datetime, timedelta

        from solstone.think.activity_state_machine import ActivityStateMachine

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

        rec = sm.get_completed_activities()[0]
        # Simulate routes.py cutoff calculation
        cutoff_ts = (datetime.now() - timedelta(hours=4)).timestamp() * 1000
        # This must not raise TypeError
        assert isinstance(rec["created_at"] < cutoff_ts, bool)
        # Also must support division (routes.py line 283)
        dt = datetime.fromtimestamp(rec["created_at"] / 1000)
        assert isinstance(dt, datetime)

    def test_level_avg_is_float(self):
        from solstone.think.activity_state_machine import ActivityStateMachine

        sm = ActivityStateMachine()
        sm.update(
            _sense(
                content_type="coding",
                facets=[{"facet": "work", "activity": "coding", "level": "high"}],
            ),
            "090000_300",
            "20260304",
        )
        sm.update(_sense(content_type="meeting"), "090500_300", "20260304")
        sm.update(_sense(content_type="meeting"), "091000_300", "20260304")

        rec = sm.get_completed_activities()[0]
        assert isinstance(rec["level_avg"], float)
        assert rec["level_avg"] == 1.0  # "high" maps to 1.0

    def test_all_required_fields_present(self):
        """Completed record must have every field run_activity_prompts expects."""
        from solstone.think.activity_state_machine import ActivityStateMachine

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

        rec = sm.get_completed_activities()[0]
        required = {
            "id",
            "facet",
            "activity",
            "segments",
            "level_avg",
            "description",
            "active_entities",
            "created_at",
        }
        assert required.issubset(rec.keys())
        # No internal _fields should leak
        assert not any(k.startswith("_") for k in rec.keys())

    def test_active_entities_preserved_in_completed(self):
        from solstone.think.activity_state_machine import ActivityStateMachine

        sm = ActivityStateMachine()
        entities = [
            {"type": "Person", "name": "Alice", "context": "dev"},
            {"type": "Tool", "name": "VSCode", "context": "editor"},
        ]
        sm.update(
            _sense(content_type="coding", entities=entities), "090000_300", "20260304"
        )
        sm.update(_sense(density="idle"), "090500_300", "20260304")

        rec = sm.get_completed_activities()[0]
        assert rec["active_entities"] == ["Alice", "VSCode"]


class TestCumulativeCompletedList:
    """Verify get_completed_activities() is cumulative and correct."""

    def test_multiple_endings_accumulate(self):
        from solstone.think.activity_state_machine import ActivityStateMachine

        sm = ActivityStateMachine()
        # Activity 1: coding
        sm.update(_sense(content_type="coding"), "090000_300", "20260304")
        sm.update(_sense(content_type="meeting"), "090500_300", "20260304")
        sm.update(_sense(content_type="meeting"), "091000_300", "20260304")
        # Activity 2: meeting
        sm.update(_sense(content_type="coding"), "091500_300", "20260304")
        sm.update(_sense(content_type="coding"), "092000_300", "20260304")

        completed = sm.get_completed_activities()
        assert len(completed) == 2
        assert completed[0]["activity"] == "coding"
        assert completed[1]["activity"] == "meeting"

    def test_completed_list_is_copy(self):
        """get_completed_activities returns a copy, not internal state."""
        from solstone.think.activity_state_machine import ActivityStateMachine

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

        list1 = sm.get_completed_activities()
        list2 = sm.get_completed_activities()
        assert list1 is not list2
        assert list1 == list2
