"""
test_personality.py — Unit tests for the saltybot personality system.

No ROS2 runtime required.  Tests pure functions from:
  - soul_loader.py
  - mood_engine.py
  - relationship_db.py

Run with:
  pytest jetson/ros2_ws/src/saltybot_social_personality/test/test_personality.py
"""

import os
import tempfile
import textwrap

import pytest

# ── Imports (pure modules, no ROS2) ──────────────────────────────────────────

import sys
sys.path.insert(
    0,
    os.path.join(os.path.dirname(__file__), "..", "saltybot_social_personality"),
)

from soul_loader import load_soul, _extract_frontmatter
from mood_engine import (
    compute_mood, get_relationship_tier, build_greeting,
    MOOD_HAPPY, MOOD_PLAYFUL, MOOD_CURIOUS, MOOD_ANNOYED,
    TIER_STRANGER, TIER_REGULAR, TIER_FAVORITE,
    EVENT_NEGATIVE, EVENT_POSITIVE, EVENT_GREETING,
)
from relationship_db import RelationshipDB


# ── Helpers ───────────────────────────────────────────────────────────────────

def _minimal_soul(**overrides) -> dict:
    """Return a valid minimal soul dict, optionally overriding keys."""
    base = {
        "name":               "Salty",
        "humor_level":        7,
        "sass_level":         4,
        "base_mood":          "playful",
        "threshold_regular":  5,
        "threshold_favorite": 20,
        "greeting_stranger":  "Hello there!",
        "greeting_regular":   "Hey {name}!",
        "greeting_favorite":  "Oh hey {name}!!",
    }
    base.update(overrides)
    return base


def _write_soul(content: str) -> str:
    """Write a SOUL.md string to a temp file and return the path."""
    fh = tempfile.NamedTemporaryFile(
        mode="w", suffix=".md", delete=False, encoding="utf-8"
    )
    fh.write(content)
    fh.close()
    return fh.name


_VALID_SOUL_CONTENT = textwrap.dedent("""\
    ---
    name: "TestBot"
    speaking_style: "casual"
    humor_level: 7
    sass_level: 3
    base_mood: "playful"
    threshold_regular: 5
    threshold_favorite: 20
    greeting_stranger: "Hello stranger!"
    greeting_regular: "Hey {name}!"
    greeting_favorite: "Oh hey {name}!!"
    mood_prefix_playful: "Beep boop! "
    mood_prefix_happy: "Great — "
    mood_prefix_curious: "Hmm, "
    mood_prefix_annoyed: "Ugh, "
    ---
    # Description (ignored)
    This is the description body.
""")


# ═══════════════════════════════════════════════════════════════════════════════
# soul_loader tests
# ═══════════════════════════════════════════════════════════════════════════════

class TestExtractFrontmatter:
    def test_delimited(self):
        content = "---\nkey: val\n---\n# body"
        assert _extract_frontmatter(content) == "key: val"

    def test_no_delimiters_returns_whole(self):
        content = "key: val\nother: 123"
        assert _extract_frontmatter(content) == content

    def test_single_delimiter_returns_whole(self):
        content = "---\nkey: val\n"
        result = _extract_frontmatter(content)
        assert "key: val" in result

    def test_body_stripped(self):
        content = "---\nname: X\n---\n# Body text\nMore body"
        assert "Body text" not in _extract_frontmatter(content)
        assert "name: X" in _extract_frontmatter(content)


class TestLoadSoul:
    def test_valid_file_loads(self):
        path = _write_soul(_VALID_SOUL_CONTENT)
        try:
            soul = load_soul(path)
            assert soul["name"] == "TestBot"
            assert soul["humor_level"] == 7
            assert soul["base_mood"] == "playful"
        finally:
            os.unlink(path)

    def test_missing_file_raises(self):
        with pytest.raises(FileNotFoundError):
            load_soul("/nonexistent/SOUL.md")

    def test_missing_required_key_raises(self):
        content = "---\nname: X\nhumor_level: 5\n---"  # missing many keys
        path = _write_soul(content)
        try:
            with pytest.raises(ValueError, match="missing required keys"):
                load_soul(path)
        finally:
            os.unlink(path)

    def test_humor_out_of_range_raises(self):
        soul_str = _VALID_SOUL_CONTENT.replace("humor_level: 7", "humor_level: 11")
        path = _write_soul(soul_str)
        try:
            with pytest.raises(ValueError, match="humor_level"):
                load_soul(path)
        finally:
            os.unlink(path)

    def test_invalid_mood_raises(self):
        soul_str = _VALID_SOUL_CONTENT.replace(
            'base_mood: "playful"', 'base_mood: "grumpy"'
        )
        path = _write_soul(soul_str)
        try:
            with pytest.raises(ValueError, match="base_mood"):
                load_soul(path)
        finally:
            os.unlink(path)

    def test_threshold_order_enforced(self):
        soul_str = _VALID_SOUL_CONTENT.replace(
            "threshold_regular: 5", "threshold_regular: 25"
        )
        path = _write_soul(soul_str)
        try:
            with pytest.raises(ValueError, match="threshold_regular"):
                load_soul(path)
        finally:
            os.unlink(path)

    def test_extra_keys_allowed(self):
        content = _VALID_SOUL_CONTENT.replace(
            "---\n# Description",
            "custom_key: 42\n---\n# Description"
        )
        path = _write_soul(content)
        try:
            soul = load_soul(path)
            assert soul.get("custom_key") == 42
        finally:
            os.unlink(path)


# ═══════════════════════════════════════════════════════════════════════════════
# mood_engine tests
# ═══════════════════════════════════════════════════════════════════════════════

class TestGetRelationshipTier:
    def test_zero_interactions_stranger(self):
        soul = _minimal_soul(threshold_regular=5, threshold_favorite=20)
        assert get_relationship_tier(soul, 0) == TIER_STRANGER

    def test_below_regular_stranger(self):
        soul = _minimal_soul(threshold_regular=5, threshold_favorite=20)
        assert get_relationship_tier(soul, 4) == TIER_STRANGER

    def test_at_regular_threshold(self):
        soul = _minimal_soul(threshold_regular=5, threshold_favorite=20)
        assert get_relationship_tier(soul, 5) == TIER_REGULAR

    def test_above_regular_below_favorite(self):
        soul = _minimal_soul(threshold_regular=5, threshold_favorite=20)
        assert get_relationship_tier(soul, 10) == TIER_REGULAR

    def test_at_favorite_threshold(self):
        soul = _minimal_soul(threshold_regular=5, threshold_favorite=20)
        assert get_relationship_tier(soul, 20) == TIER_FAVORITE

    def test_above_favorite(self):
        soul = _minimal_soul(threshold_regular=5, threshold_favorite=20)
        assert get_relationship_tier(soul, 100) == TIER_FAVORITE


class TestComputeMood:
    def test_unknown_person_returns_curious(self):
        soul = _minimal_soul(humor_level=7)
        mood = compute_mood(soul, score=0.0, interaction_count=0, recent_events=[])
        assert mood == MOOD_CURIOUS

    def test_stranger_low_count_returns_curious(self):
        soul = _minimal_soul(threshold_regular=5)
        mood = compute_mood(soul, score=2.0, interaction_count=3, recent_events=[])
        assert mood == MOOD_CURIOUS

    def test_two_negative_events_returns_annoyed(self):
        soul = _minimal_soul(threshold_regular=5)
        events = [
            {"type": EVENT_NEGATIVE, "dt": 30.0},
            {"type": EVENT_NEGATIVE, "dt": 60.0},
        ]
        mood = compute_mood(soul, score=10.0, interaction_count=10, recent_events=events)
        assert mood == MOOD_ANNOYED

    def test_one_negative_not_annoyed(self):
        soul = _minimal_soul(humor_level=7, threshold_regular=5, threshold_favorite=20)
        events = [{"type": EVENT_NEGATIVE, "dt": 30.0}]
        # 1 negative is not enough → should still be happy/playful based on score
        mood = compute_mood(soul, score=25.0, interaction_count=25, recent_events=events)
        assert mood != MOOD_ANNOYED

    def test_high_humor_regular_returns_playful(self):
        soul = _minimal_soul(humor_level=8, threshold_regular=5, threshold_favorite=20)
        events = [{"type": EVENT_POSITIVE, "dt": 10.0}]
        mood = compute_mood(soul, score=10.0, interaction_count=8, recent_events=events)
        assert mood == MOOD_PLAYFUL

    def test_low_humor_regular_returns_happy(self):
        soul = _minimal_soul(humor_level=4, threshold_regular=5, threshold_favorite=20)
        events = [{"type": EVENT_POSITIVE, "dt": 10.0}]
        mood = compute_mood(soul, score=10.0, interaction_count=8, recent_events=events)
        assert mood == MOOD_HAPPY

    def test_stale_negative_ignored(self):
        soul = _minimal_soul(humor_level=8, threshold_regular=5, threshold_favorite=20)
        # dt > 120s → outside the recent window → should not trigger annoyed
        events = [
            {"type": EVENT_NEGATIVE, "dt": 200.0},
            {"type": EVENT_NEGATIVE, "dt": 300.0},
        ]
        mood = compute_mood(soul, score=15.0, interaction_count=10, recent_events=events)
        assert mood != MOOD_ANNOYED

    def test_favorite_high_humor_playful(self):
        soul = _minimal_soul(humor_level=9, threshold_regular=5, threshold_favorite=20)
        mood = compute_mood(soul, score=50.0, interaction_count=30, recent_events=[])
        assert mood == MOOD_PLAYFUL

    def test_favorite_low_humor_happy(self):
        soul = _minimal_soul(humor_level=3, threshold_regular=5, threshold_favorite=20)
        mood = compute_mood(soul, score=50.0, interaction_count=30, recent_events=[])
        assert mood == MOOD_HAPPY


class TestBuildGreeting:
    def _soul(self, **kw):
        return _minimal_soul(
            mood_prefix_happy="Great — ",
            mood_prefix_curious="Hmm, ",
            mood_prefix_annoyed="Well, ",
            mood_prefix_playful="Beep boop! ",
            **kw,
        )

    def test_stranger_greeting(self):
        soul = self._soul()
        g = build_greeting(soul, TIER_STRANGER, MOOD_CURIOUS, "")
        assert "hello" in g.lower()

    def test_regular_greeting_contains_name(self):
        soul = self._soul()
        g = build_greeting(soul, TIER_REGULAR, MOOD_HAPPY, "alice")
        assert "alice" in g

    def test_favorite_greeting_contains_name(self):
        soul = self._soul()
        g = build_greeting(soul, TIER_FAVORITE, MOOD_PLAYFUL, "bob")
        assert "bob" in g

    def test_mood_prefix_applied(self):
        soul = self._soul()
        g = build_greeting(soul, TIER_REGULAR, MOOD_PLAYFUL, "alice")
        assert g.startswith("Beep boop!")

    def test_no_prefix_key_no_prefix(self):
        soul = _minimal_soul()  # no mood_prefix_* keys
        g = build_greeting(soul, TIER_REGULAR, MOOD_HAPPY, "alice")
        assert g.startswith("Hey")

    def test_empty_person_id_uses_friend(self):
        soul = self._soul()
        g = build_greeting(soul, TIER_REGULAR, MOOD_HAPPY, "")
        assert "friend" in g

    def test_happy_prefix(self):
        soul = self._soul()
        g = build_greeting(soul, TIER_REGULAR, MOOD_HAPPY, "carol")
        assert g.startswith("Great")

    def test_annoyed_prefix(self):
        soul = self._soul()
        g = build_greeting(soul, TIER_REGULAR, MOOD_ANNOYED, "dave")
        assert g.startswith("Well")


# ═══════════════════════════════════════════════════════════════════════════════
# relationship_db tests
# ═══════════════════════════════════════════════════════════════════════════════

class TestRelationshipDB:
    @pytest.fixture
    def db(self, tmp_path):
        path = str(tmp_path / "test.db")
        d = RelationshipDB(path)
        yield d
        d.close()

    def test_get_person_creates_default(self, db):
        p = db.get_person("alice")
        assert p["person_id"] == "alice"
        assert p["score"] == pytest.approx(0.0)
        assert p["interaction_count"] == 0

    def test_get_person_idempotent(self, db):
        p1 = db.get_person("bob")
        p2 = db.get_person("bob")
        assert p1["person_id"] == p2["person_id"]

    def test_record_interaction_increments_count(self, db):
        db.record_interaction("alice", "detection")
        db.record_interaction("alice", "detection")
        p = db.get_person("alice")
        assert p["interaction_count"] == 2

    def test_record_interaction_updates_score(self, db):
        db.record_interaction("alice", "positive", delta_score=5.0)
        p = db.get_person("alice")
        assert p["score"] == pytest.approx(5.0)

    def test_negative_delta_reduces_score(self, db):
        db.record_interaction("carol", "positive",  delta_score=10.0)
        db.record_interaction("carol", "negative",  delta_score=-3.0)
        p = db.get_person("carol")
        assert p["score"] == pytest.approx(7.0)

    def test_score_zero_by_default(self, db):
        p = db.get_person("dave")
        assert p["score"] == pytest.approx(0.0)

    def test_set_and_get_pref(self, db):
        db.set_pref("alice", "language", "en")
        assert db.get_pref("alice", "language") == "en"

    def test_get_pref_default(self, db):
        assert db.get_pref("nobody", "language", "fr") == "fr"

    def test_multiple_prefs_stored(self, db):
        db.set_pref("alice", "lang", "en")
        db.set_pref("alice", "name", "Alice")
        assert db.get_pref("alice", "lang") == "en"
        assert db.get_pref("alice", "name") == "Alice"

    def test_all_people_returns_list(self, db):
        db.record_interaction("a", "detection")
        db.record_interaction("b", "detection")
        people = db.all_people()
        ids = {p["person_id"] for p in people}
        assert {"a", "b"} <= ids

    def test_get_recent_events_returns_events(self, db):
        db.record_interaction("alice", "greeting", delta_score=1.0)
        events = db.get_recent_events("alice", window_s=60.0)
        assert len(events) == 1
        assert events[0]["type"] == "greeting"

    def test_get_recent_events_empty_for_new_person(self, db):
        events = db.get_recent_events("nobody", window_s=60.0)
        assert events == []

    def test_event_dt_positive(self, db):
        db.record_interaction("alice", "detection")
        events = db.get_recent_events("alice", window_s=60.0)
        assert events[0]["dt"] >= 0.0

    def test_multiple_people_isolated(self, db):
        db.record_interaction("alice", "positive", delta_score=10.0)
        db.record_interaction("bob",   "negative", delta_score=-5.0)
        assert db.get_person("alice")["score"] == pytest.approx(10.0)
        assert db.get_person("bob")["score"]   == pytest.approx(-5.0)

    def test_details_stored(self, db):
        db.record_interaction("alice", "greeting", details={"location": "lab"})
        events = db.get_recent_events("alice", window_s=60.0)
        assert events[0]["details"].get("location") == "lab"


# ═══════════════════════════════════════════════════════════════════════════════
# Integration: soul → tier → mood → greeting pipeline
# ═══════════════════════════════════════════════════════════════════════════════

class TestIntegrationPipeline:

    def test_stranger_pipeline(self, tmp_path):
        db_path = str(tmp_path / "int.db")
        db = RelationshipDB(db_path)
        soul = _minimal_soul(
            humor_level=7, threshold_regular=5, threshold_favorite=20,
            mood_prefix_curious="Hmm, "
        )
        # No prior interactions
        person = db.get_person("stranger_001")
        events = db.get_recent_events("stranger_001")
        tier    = get_relationship_tier(soul, person["interaction_count"])
        mood    = compute_mood(soul, person["score"], person["interaction_count"], events)
        greeting = build_greeting(soul, tier, mood, "stranger_001")

        assert tier == TIER_STRANGER
        assert mood == MOOD_CURIOUS
        assert "hello" in greeting.lower()
        db.close()

    def test_regular_positive_pipeline(self, tmp_path):
        db_path = str(tmp_path / "int2.db")
        db = RelationshipDB(db_path)
        soul = _minimal_soul(
            humor_level=8, threshold_regular=5, threshold_favorite=20,
            mood_prefix_playful="Beep! "
        )
        # Simulate 6 positive interactions (> threshold_regular=5)
        for _ in range(6):
            db.record_interaction("alice", "positive", delta_score=2.0)

        person = db.get_person("alice")
        events = db.get_recent_events("alice")
        tier    = get_relationship_tier(soul, person["interaction_count"])
        mood    = compute_mood(soul, person["score"], person["interaction_count"], events)
        greeting = build_greeting(soul, tier, mood, "alice")

        assert tier == TIER_REGULAR
        assert mood == MOOD_PLAYFUL     # humor_level=8, recent positive events
        assert "alice" in greeting
        assert greeting.startswith("Beep!")
        db.close()

    def test_favorite_pipeline(self, tmp_path):
        db_path = str(tmp_path / "int3.db")
        db = RelationshipDB(db_path)
        soul = _minimal_soul(
            humor_level=5, threshold_regular=5, threshold_favorite=20
        )
        for _ in range(25):
            db.record_interaction("bob", "positive", delta_score=1.0)

        person = db.get_person("bob")
        tier    = get_relationship_tier(soul, person["interaction_count"])
        assert tier == TIER_FAVORITE
        greeting = build_greeting(soul, tier, "happy", "bob")
        assert "bob" in greeting
        assert "Oh hey" in greeting
        db.close()
