"""Tests for Workshop presence heartbeat."""

import json
from unittest.mock import patch

import pytest

from timmy.workshop_state import (
    WorkshopHeartbeat,
    _state_hash,
    get_state_dict,
    write_state,
)

# ---------------------------------------------------------------------------
# get_state_dict
# ---------------------------------------------------------------------------


def test_get_state_dict_returns_v1_schema():
    state = get_state_dict()
    assert state["version"] == 1
    assert "liveness" in state
    assert "current_focus" in state
    assert "mood" in state
    assert isinstance(state["active_threads"], list)
    assert isinstance(state["recent_events"], list)
    assert isinstance(state["concerns"], list)
    # Issue #360 enriched fields
    assert isinstance(state["confidence"], float)
    assert 0.0 <= state["confidence"] <= 1.0
    assert isinstance(state["energy"], float)
    assert 0.0 <= state["energy"] <= 1.0
    assert state["identity"]["name"] == "Timmy"
    assert state["identity"]["title"] == "The Workshop Wizard"
    assert isinstance(state["identity"]["uptime_seconds"], int)
    assert state["activity"]["current"] in ("idle", "thinking")
    assert state["environment"]["time_of_day"] in (
        "morning",
        "afternoon",
        "evening",
        "night",
        "deep-night",
    )
    assert state["environment"]["day_of_week"] in (
        "Monday",
        "Tuesday",
        "Wednesday",
        "Thursday",
        "Friday",
        "Saturday",
        "Sunday",
    )
    assert state["interaction"]["visitor_present"] is False
    assert isinstance(state["interaction"]["conversation_turns"], int)
    assert state["meta"]["schema_version"] == 1
    assert state["meta"]["writer"] == "timmy-loop"
    assert "updated_at" in state["meta"]


def test_get_state_dict_idle_mood():
    """Idle engagement + settled mood → 'calm' presence mood."""
    from timmy.cognitive_state import CognitiveState, CognitiveTracker

    tracker = CognitiveTracker.__new__(CognitiveTracker)
    tracker.state = CognitiveState(engagement="idle", mood="settled")
    with patch("timmy.cognitive_state.cognitive_tracker", tracker):
        state = get_state_dict()
    assert state["mood"] == "calm"


def test_get_state_dict_maps_mood():
    """Cognitive moods map to presence moods."""
    from timmy.cognitive_state import CognitiveState, CognitiveTracker

    for cog_mood, expected in [
        ("curious", "contemplative"),
        ("hesitant", "uncertain"),
        ("energized", "excited"),
    ]:
        tracker = CognitiveTracker.__new__(CognitiveTracker)
        tracker.state = CognitiveState(engagement="deep", mood=cog_mood)
        with patch("timmy.cognitive_state.cognitive_tracker", tracker):
            state = get_state_dict()
        assert state["mood"] == expected, f"Expected {expected} for {cog_mood}"


# ---------------------------------------------------------------------------
# write_state
# ---------------------------------------------------------------------------


def test_write_state_creates_file(tmp_path):
    target = tmp_path / "presence.json"
    state = {"version": 1, "liveness": "2026-01-01T00:00:00Z", "current_focus": ""}
    write_state(state, path=target)

    assert target.exists()
    data = json.loads(target.read_text())
    assert data["version"] == 1


def test_write_state_creates_parent_dirs(tmp_path):
    target = tmp_path / "deep" / "nested" / "presence.json"
    write_state({"version": 1}, path=target)
    assert target.exists()


# ---------------------------------------------------------------------------
# _state_hash
# ---------------------------------------------------------------------------


def test_state_hash_ignores_liveness():
    a = {"version": 1, "mood": "focused", "liveness": "2026-01-01T00:00:00Z"}
    b = {"version": 1, "mood": "focused", "liveness": "2026-12-31T23:59:59Z"}
    assert _state_hash(a) == _state_hash(b)


def test_state_hash_detects_mood_change():
    a = {"version": 1, "mood": "focused", "liveness": "t1"}
    b = {"version": 1, "mood": "idle", "liveness": "t1"}
    assert _state_hash(a) != _state_hash(b)


# ---------------------------------------------------------------------------
# WorkshopHeartbeat — _write_if_changed
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_write_if_changed_writes_on_first_call(tmp_path):
    target = tmp_path / "presence.json"
    hb = WorkshopHeartbeat(interval=60, path=target)

    with patch("timmy.workshop_state.get_state_dict") as mock_state:
        mock_state.return_value = {
            "version": 1,
            "liveness": "t1",
            "current_focus": "testing",
            "mood": "focused",
        }
        await hb._write_if_changed()

    assert target.exists()
    data = json.loads(target.read_text())
    assert data["version"] == 1
    assert data["current_focus"] == "testing"


@pytest.mark.asyncio
async def test_write_if_changed_skips_when_unchanged(tmp_path):
    target = tmp_path / "presence.json"
    hb = WorkshopHeartbeat(interval=60, path=target)

    fixed_state = {"version": 1, "liveness": "t1", "mood": "idle"}

    with patch("timmy.workshop_state.get_state_dict", return_value=fixed_state):
        await hb._write_if_changed()  # First write
        target.write_text("")  # Clear to detect if second write happens
        await hb._write_if_changed()  # Should skip — state unchanged

    # File should still be empty (second write was skipped)
    assert target.read_text() == ""


@pytest.mark.asyncio
async def test_write_if_changed_writes_on_state_change(tmp_path):
    target = tmp_path / "presence.json"
    hb = WorkshopHeartbeat(interval=60, path=target)

    state_a = {"version": 1, "liveness": "t1", "mood": "idle"}
    state_b = {"version": 1, "liveness": "t2", "mood": "focused"}

    with patch("timmy.workshop_state.get_state_dict", return_value=state_a):
        await hb._write_if_changed()

    with patch("timmy.workshop_state.get_state_dict", return_value=state_b):
        await hb._write_if_changed()

    data = json.loads(target.read_text())
    assert data["mood"] == "focused"


@pytest.mark.asyncio
async def test_write_if_changed_calls_on_change(tmp_path):
    """on_change callback is invoked with state dict when state changes."""
    target = tmp_path / "presence.json"
    received = []

    async def capture(state_dict):
        received.append(state_dict)

    hb = WorkshopHeartbeat(interval=60, path=target, on_change=capture)

    state = {"version": 1, "liveness": "t1", "mood": "focused"}
    with patch("timmy.workshop_state.get_state_dict", return_value=state):
        await hb._write_if_changed()

    assert len(received) == 1
    assert received[0]["mood"] == "focused"


@pytest.mark.asyncio
async def test_write_if_changed_skips_on_change_when_unchanged(tmp_path):
    """on_change is NOT called when state hash is unchanged."""
    target = tmp_path / "presence.json"
    call_count = 0

    async def counter(_):
        nonlocal call_count
        call_count += 1

    hb = WorkshopHeartbeat(interval=60, path=target, on_change=counter)

    state = {"version": 1, "liveness": "t1", "mood": "idle"}
    with patch("timmy.workshop_state.get_state_dict", return_value=state):
        await hb._write_if_changed()
        await hb._write_if_changed()

    assert call_count == 1


# ---------------------------------------------------------------------------
# WorkshopHeartbeat — notify
# ---------------------------------------------------------------------------


def test_notify_sets_trigger():
    hb = WorkshopHeartbeat(interval=60)
    assert not hb._trigger.is_set()
    hb.notify()
    assert hb._trigger.is_set()


# ---------------------------------------------------------------------------
# WorkshopHeartbeat — start/stop lifecycle
# ---------------------------------------------------------------------------


@pytest.mark.asyncio
async def test_heartbeat_start_stop_lifecycle(tmp_path):
    target = tmp_path / "presence.json"
    hb = WorkshopHeartbeat(interval=60, path=target)

    with patch("timmy.workshop_state.get_state_dict", return_value={"version": 1}):
        await hb.start()
        assert hb._task is not None
        assert not hb._task.done()
        await hb.stop()
        assert hb._task is None
