"""Tests for kokoro_chat.attitude (levels + state)."""
from __future__ import annotations

from datetime import datetime, timedelta, timezone

import pytest

from kokoro_chat.attitude.levels import DEFAULT_AFFECTION_LEVELS, get_affection_level
from kokoro_chat.attitude.state import AttitudeState


# ---------------------------------------------------------------------------
# get_affection_level
# ---------------------------------------------------------------------------

class TestGetAffectionLevel:
    """Tests for get_affection_level with default and custom levels."""

    @pytest.mark.parametrize(
        "score, expected",
        [
            (0, "stranger"),
            (10, "stranger"),
            (20, "stranger"),
            (21, "friend"),
            (30, "friend"),
            (40, "friend"),
            (41, "crush"),
            (50, "crush"),
            (60, "crush"),
            (61, "lover"),
            (70, "lover"),
            (80, "lover"),
            (81, "married"),
            (90, "married"),
            (100, "married"),
        ],
    )
    def test_boundary_values(self, score: int, expected: str):
        assert get_affection_level(score) == expected

    @pytest.mark.parametrize("score", [-1, -100, 101, 200])
    def test_out_of_range_returns_stranger(self, score: int):
        assert get_affection_level(score) == "stranger"

    def test_custom_levels(self):
        custom = {"cold": (0, 50), "warm": (51, 100)}
        assert get_affection_level(25, levels=custom) == "cold"
        assert get_affection_level(50, levels=custom) == "cold"
        assert get_affection_level(51, levels=custom) == "warm"
        assert get_affection_level(100, levels=custom) == "warm"

    def test_custom_levels_out_of_range_fallback(self):
        custom = {"only": (10, 20)}
        assert get_affection_level(5, levels=custom) == "stranger"

    def test_default_levels_cover_full_range(self):
        """Every integer 0-100 should map to exactly one level."""
        for score in range(101):
            result = get_affection_level(score)
            assert result in DEFAULT_AFFECTION_LEVELS


# ---------------------------------------------------------------------------
# AttitudeState.__init__
# ---------------------------------------------------------------------------

class TestAttitudeStateInit:
    def test_defaults(self):
        state = AttitudeState("char_1", "user_1")
        assert state.character_id == "char_1"
        assert state.user_id == "user_1"
        assert state.affection == 50
        assert state.trust == 50
        assert state.mood == "neutral"
        assert state.emotional_state == ""
        assert state.attitude_toward_user == ""
        assert state.recent_shift == ""
        assert state.attitude_modifiers == []
        assert state.history == []
        assert state.last_interaction_at is None
        assert state.cooldown_hint_needed is False


# ---------------------------------------------------------------------------
# apply_time_decay
# ---------------------------------------------------------------------------

class TestApplyTimeDecay:
    def _make_state(self, affection: int = 50, trust: int = 50) -> AttitudeState:
        s = AttitudeState("c", "u")
        s.affection = affection
        s.trust = trust
        return s

    def test_no_last_interaction_returns_false(self):
        state = self._make_state()
        assert state.apply_time_decay() is False

    def test_less_than_3_days_returns_false(self):
        state = self._make_state()
        now = datetime(2025, 6, 1, 12, 0, tzinfo=timezone.utc)
        state.last_interaction_at = now - timedelta(days=2, hours=23)
        assert state.apply_time_decay(now=now) is False
        assert state.affection == 50
        assert state.trust == 50

    def test_exactly_3_days_applies_decay(self):
        state = self._make_state()
        now = datetime(2025, 6, 4, 12, 0, tzinfo=timezone.utc)
        state.last_interaction_at = datetime(2025, 6, 1, 12, 0, tzinfo=timezone.utc)
        result = state.apply_time_decay(now=now)
        # days_since=3, affection_decay=min(1.5, max(0, 50-30))=1.5
        # trust_decay=min(0.6, max(0, 50-20))=0.6
        assert result is True
        assert state.affection == 48  # 50 - round(1.5) = 48
        assert state.trust == 49      # 50 - round(0.6) = 49
        assert state.cooldown_hint_needed is True

    def test_10_days_larger_decay(self):
        state = self._make_state(affection=60, trust=60)
        now = datetime(2025, 6, 11, 0, 0, tzinfo=timezone.utc)
        state.last_interaction_at = datetime(2025, 6, 1, 0, 0, tzinfo=timezone.utc)
        state.apply_time_decay(now=now)
        # days_since=10
        # affection_decay = min(10*0.5, max(0, 60-30)) = min(5, 30) = 5
        # trust_decay = min(10*0.2, max(0, 60-20)) = min(2, 40) = 2
        assert state.affection == 55  # 60 - 5
        assert state.trust == 58      # 60 - 2

    def test_floor_clamping_affection(self):
        """Affection should never drop below 30."""
        state = self._make_state(affection=31, trust=50)
        now = datetime(2025, 7, 1, 0, 0, tzinfo=timezone.utc)
        state.last_interaction_at = datetime(2025, 6, 1, 0, 0, tzinfo=timezone.utc)
        state.apply_time_decay(now=now)
        assert state.affection >= 30

    def test_floor_clamping_trust(self):
        """Trust should never drop below 20."""
        state = self._make_state(affection=50, trust=21)
        now = datetime(2025, 7, 1, 0, 0, tzinfo=timezone.utc)
        state.last_interaction_at = datetime(2025, 6, 1, 0, 0, tzinfo=timezone.utc)
        state.apply_time_decay(now=now)
        assert state.trust >= 20

    def test_already_at_floor_no_change(self):
        """If already at floor values, no decay occurs and returns False."""
        state = self._make_state(affection=30, trust=20)
        now = datetime(2025, 7, 1, 0, 0, tzinfo=timezone.utc)
        state.last_interaction_at = datetime(2025, 6, 1, 0, 0, tzinfo=timezone.utc)
        result = state.apply_time_decay(now=now)
        assert state.affection == 30
        assert state.trust == 20
        assert result is False
        assert state.cooldown_hint_needed is False

    def test_naive_datetime_treated_as_utc(self):
        """A naive last_interaction_at should be treated as UTC."""
        state = self._make_state()
        now = datetime(2025, 6, 6, 0, 0, tzinfo=timezone.utc)
        state.last_interaction_at = datetime(2025, 6, 1, 0, 0)  # naive
        result = state.apply_time_decay(now=now)
        # 5 days, should decay
        assert result is True
        assert state.affection < 50


# ---------------------------------------------------------------------------
# apply_event_impact
# ---------------------------------------------------------------------------

class TestApplyEventImpact:
    def test_positive_deltas(self):
        state = AttitudeState("c", "u")
        state.apply_event_impact("gift", {"affection_delta": 10, "trust_delta": 5})
        assert state.affection == 60
        assert state.trust == 55

    def test_negative_deltas(self):
        state = AttitudeState("c", "u")
        state.apply_event_impact("insult", {"affection_delta": -20, "trust_delta": -15})
        assert state.affection == 30
        assert state.trust == 35

    def test_clamp_upper_bound(self):
        state = AttitudeState("c", "u")
        state.affection = 95
        state.trust = 98
        state.apply_event_impact("amazing", {"affection_delta": 20, "trust_delta": 10})
        assert state.affection == 100
        assert state.trust == 100

    def test_clamp_lower_bound(self):
        state = AttitudeState("c", "u")
        state.affection = 5
        state.trust = 3
        state.apply_event_impact("betrayal", {"affection_delta": -50, "trust_delta": -50})
        assert state.affection == 0
        assert state.trust == 0

    def test_mood_change(self):
        state = AttitudeState("c", "u")
        state.apply_event_impact("joke", {"new_mood": "happy"})
        assert state.mood == "happy"

    def test_no_mood_key_keeps_existing(self):
        state = AttitudeState("c", "u")
        state.mood = "sad"
        state.apply_event_impact("event", {"affection_delta": 1})
        assert state.mood == "sad"

    def test_modifier_created_with_default_duration(self):
        state = AttitudeState("c", "u")
        state.apply_event_impact("praise", {"attitude_shift": "feels appreciated"})
        assert len(state.attitude_modifiers) == 1
        mod = state.attitude_modifiers[0]
        assert mod["trigger"] == "praise"
        assert mod["effect"] == "feels appreciated"
        assert mod["remaining"] == 5

    def test_modifier_created_with_custom_duration(self):
        state = AttitudeState("c", "u")
        state.apply_event_impact("gift", {"attitude_shift": "grateful", "duration": 10})
        assert state.attitude_modifiers[0]["remaining"] == 10

    def test_recent_shift_set(self):
        state = AttitudeState("c", "u")
        state.apply_event_impact("compliment", {"attitude_shift": "blushing"})
        assert "compliment" in state.recent_shift
        assert "blushing" in state.recent_shift

    def test_history_recorded(self):
        state = AttitudeState("c", "u")
        state.apply_event_impact("event_x", {"affection_delta": 5})
        assert len(state.history) == 1
        entry = state.history[0]
        assert entry["event"] == "event_x"
        assert "time" in entry
        assert "impact" in entry
        assert "resulting_state" in entry

    def test_no_delta_keys_leaves_values_unchanged(self):
        state = AttitudeState("c", "u")
        state.apply_event_impact("nothing", {})
        assert state.affection == 50
        assert state.trust == 50


# ---------------------------------------------------------------------------
# decay_modifiers
# ---------------------------------------------------------------------------

class TestDecayModifiers:
    def test_countdown(self):
        state = AttitudeState("c", "u")
        state.attitude_modifiers = [
            {"trigger": "a", "effect": "e1", "remaining": 3},
        ]
        state.recent_shift = "something"
        state.decay_modifiers()
        assert len(state.attitude_modifiers) == 1
        assert state.attitude_modifiers[0]["remaining"] == 2

    def test_removal_at_zero(self):
        state = AttitudeState("c", "u")
        state.attitude_modifiers = [
            {"trigger": "a", "effect": "e1", "remaining": 1},
        ]
        state.recent_shift = "shift"
        state.decay_modifiers()
        assert len(state.attitude_modifiers) == 0

    def test_recent_shift_cleared_when_all_gone(self):
        state = AttitudeState("c", "u")
        state.attitude_modifiers = [
            {"trigger": "a", "effect": "e1", "remaining": 1},
        ]
        state.recent_shift = "some shift"
        state.decay_modifiers()
        assert state.recent_shift == ""

    def test_recent_shift_kept_when_modifiers_remain(self):
        state = AttitudeState("c", "u")
        state.attitude_modifiers = [
            {"trigger": "a", "effect": "e1", "remaining": 1},
            {"trigger": "b", "effect": "e2", "remaining": 3},
        ]
        state.recent_shift = "shift reason"
        state.decay_modifiers()
        # One removed, one remains
        assert len(state.attitude_modifiers) == 1
        assert state.recent_shift == "shift reason"

    def test_multiple_decays_to_empty(self):
        state = AttitudeState("c", "u")
        state.attitude_modifiers = [
            {"trigger": "a", "effect": "e1", "remaining": 2},
        ]
        state.recent_shift = "x"
        state.decay_modifiers()
        assert len(state.attitude_modifiers) == 1
        state.decay_modifiers()
        assert len(state.attitude_modifiers) == 0
        assert state.recent_shift == ""


# ---------------------------------------------------------------------------
# snapshot
# ---------------------------------------------------------------------------

class TestSnapshot:
    def test_keys_and_defaults(self):
        state = AttitudeState("c", "u")
        snap = state.snapshot()
        assert snap == {
            "affection": 50,
            "trust": 50,
            "mood": "neutral",
            "emotional_state": "",
            "attitude_toward_user": "",
            "active_modifiers": [],
        }

    def test_reflects_mutations(self):
        state = AttitudeState("c", "u")
        state.affection = 80
        state.trust = 70
        state.mood = "excited"
        state.emotional_state = "thrilled"
        state.attitude_toward_user = "adoring"
        state.attitude_modifiers = [
            {"trigger": "t", "effect": "happy boost", "remaining": 2},
        ]
        snap = state.snapshot()
        assert snap["affection"] == 80
        assert snap["trust"] == 70
        assert snap["mood"] == "excited"
        assert snap["emotional_state"] == "thrilled"
        assert snap["attitude_toward_user"] == "adoring"
        assert snap["active_modifiers"] == ["happy boost"]


# ---------------------------------------------------------------------------
# to_prompt_section
# ---------------------------------------------------------------------------

class TestToPromptSection:
    def test_base_lines_always_present(self):
        state = AttitudeState("c", "u")
        text = state.to_prompt_section()
        assert "50/100" in text
        assert "neutral" in text
        # Lines count: affection, trust, mood = 3
        lines = text.strip().split("\n")
        assert len(lines) == 3

    def test_emotional_state_included(self):
        state = AttitudeState("c", "u")
        state.emotional_state = "anxious"
        text = state.to_prompt_section()
        assert "anxious" in text

    def test_attitude_toward_user_included(self):
        state = AttitudeState("c", "u")
        state.attitude_toward_user = "curious"
        text = state.to_prompt_section()
        assert "curious" in text

    def test_recent_shift_included(self):
        state = AttitudeState("c", "u")
        state.recent_shift = "something happened"
        text = state.to_prompt_section()
        assert "something happened" in text

    def test_modifiers_included(self):
        state = AttitudeState("c", "u")
        state.attitude_modifiers = [
            {"trigger": "a", "effect": "effect_a", "remaining": 2},
            {"trigger": "b", "effect": "effect_b", "remaining": 1},
        ]
        text = state.to_prompt_section()
        assert "effect_a" in text
        assert "effect_b" in text

    def test_empty_optional_fields_omitted(self):
        state = AttitudeState("c", "u")
        text = state.to_prompt_section()
        # These Chinese labels should NOT appear when fields are empty
        assert "\u60c5\u7dd2\u72c0\u614b" not in text
        assert "\u5c0d\u7528\u6236\u7684\u614b\u5ea6" not in text
        assert "\u6700\u8fd1\u7684\u614b\u5ea6\u8b8a\u5316" not in text
        assert "\u7576\u524d\u53d7\u5230\u7684\u5f71\u97ff" not in text

    def test_all_fields_populated(self):
        state = AttitudeState("c", "u")
        state.affection = 75
        state.trust = 60
        state.mood = "happy"
        state.emotional_state = "joyful"
        state.attitude_toward_user = "warm"
        state.recent_shift = "got a gift"
        state.attitude_modifiers = [
            {"trigger": "gift", "effect": "grateful", "remaining": 3},
        ]
        text = state.to_prompt_section()
        lines = text.strip().split("\n")
        # 3 base + emotional_state + attitude_toward_user + recent_shift + modifiers = 7
        assert len(lines) == 7
        assert "75/100" in text
        assert "60/100" in text
        assert "happy" in text
        assert "joyful" in text
        assert "warm" in text
        assert "got a gift" in text
        assert "grateful" in text
