"""test_voice_command_parser.py — Unit tests for voice_command_parser (Issue #137).

Tests cover:
  - All 30+ named intents with multiple phrasings
  - Entity extraction: location, name, mode, level, route name
  - Case insensitivity and punctuation stripping
  - requires_confirmation flag on dangerous commands
  - Fallback for unrecognised text
  - Edge cases: empty string, numbers-only, partial phrases

No ROS2 runtime required.
"""

import pytest
from saltybot_social.voice_command_parser import (
    ParsedIntent,
    _normalize,
    _normalize_volume,
    _mode_from_text,
    parse,
)


# ── Normalisation helpers ─────────────────────────────────────────────────────


class TestNormalize:
    def test_lowercase(self):
        assert _normalize("GO TO THE KITCHEN") == "go to the kitchen"

    def test_strip_punctuation(self):
        assert _normalize("stop!") == "stop"

    def test_collapse_whitespace(self):
        assert _normalize("  go   home  ") == "go home"

    def test_keep_apostrophe(self):
        assert _normalize("what's my name") == "what's my name"

    def test_keep_hyphen(self):
        assert _normalize("well-known") == "well-known"

    def test_empty_string(self):
        assert _normalize("") == ""

    def test_comma_stripped(self):
        assert _normalize("go, go, go") == "go go go"


class TestNormalizeVolume:
    def test_numeric(self):
        assert _normalize_volume("50") == "50"

    def test_percent(self):
        assert _normalize_volume("75 percent") == "75"

    def test_word_low(self):
        assert _normalize_volume("low") == "20"

    def test_word_medium(self):
        assert _normalize_volume("medium") == "50"

    def test_word_high(self):
        assert _normalize_volume("high") == "80"

    def test_word_max(self):
        assert _normalize_volume("max") == "100"

    def test_clamp_above_100(self):
        assert _normalize_volume("150") == "100"

    def test_clamp_below_0(self):
        assert _normalize_volume("-10") == "0"


# ── Nav intents ───────────────────────────────────────────────────────────────


class TestNavGoTo:
    def test_basic(self):
        r = parse("go to the kitchen")
        assert r.intent == "nav.go_to"
        assert r.entities.get("location") == "kitchen"

    def test_navigate_to(self):
        r = parse("navigate to the living room")
        assert r.intent == "nav.go_to"
        assert r.entities.get("location") == "living room"

    def test_take_me_to(self):
        r = parse("take me to the front door")
        assert r.intent == "nav.go_to"
        assert r.entities.get("location") == "front door"

    def test_head_to(self):
        r = parse("head to the bedroom")
        assert r.intent == "nav.go_to"
        assert r.entities.get("location") == "bedroom"

    def test_move_to(self):
        r = parse("Move to the garage")
        assert r.intent == "nav.go_to"
        assert r.entities.get("location") == "garage"

    def test_with_please(self):
        r = parse("go to the office please")
        assert r.intent == "nav.go_to"

    def test_go_home_does_not_match(self):
        # "go home" should be nav.go_home, not nav.go_to with location=home
        r = parse("go home")
        assert r.intent == "nav.go_home"
        assert "location" not in r.entities

    def test_confidence(self):
        assert parse("go to the lab").confidence == 0.95


class TestNavGoHome:
    def test_go_home(self):
        assert parse("go home").intent == "nav.go_home"

    def test_return_home(self):
        assert parse("return home").intent == "nav.go_home"

    def test_head_home(self):
        assert parse("head home").intent == "nav.go_home"

    def test_return_to_base(self):
        assert parse("return to base").intent == "nav.go_home"

    def test_back_to_base(self):
        assert parse("back to base").intent == "nav.go_home"

    def test_dock_yourself(self):
        assert parse("dock yourself").intent == "nav.go_home"

    def test_return_to_charging_station(self):
        assert parse("return to charging station").intent == "nav.go_home"


class TestNavFollowMe:
    def test_follow_me(self):
        assert parse("follow me").intent == "nav.follow_me"

    def test_come_with_me(self):
        assert parse("come with me").intent == "nav.follow_me"

    def test_stick_with_me(self):
        assert parse("stick with me").intent == "nav.follow_me"

    def test_walk_with_me(self):
        assert parse("walk with me").intent == "nav.follow_me"

    def test_follow_route_does_not_match(self):
        # "follow route X" should be nav.replay_route, not nav.follow_me
        r = parse("follow route alpha")
        assert r.intent == "nav.replay_route"


class TestNavStop:
    def test_stop(self):
        assert parse("stop").intent == "nav.stop"

    def test_halt(self):
        assert parse("halt").intent == "nav.stop"

    def test_freeze(self):
        assert parse("freeze").intent == "nav.stop"

    def test_stop_moving(self):
        assert parse("stop moving").intent == "nav.stop"

    def test_dont_move(self):
        assert parse("don't move").intent == "nav.stop"

    def test_hold_it(self):
        assert parse("hold it").intent == "nav.stop"

    def test_stop_right_there(self):
        assert parse("stop right there").intent == "nav.stop"


class TestNavWait:
    def test_wait(self):
        assert parse("wait").intent == "nav.wait"

    def test_wait_here(self):
        assert parse("wait here").intent == "nav.wait"

    def test_stay_here(self):
        assert parse("stay here").intent == "nav.wait"

    def test_stay_put(self):
        assert parse("stay put").intent == "nav.wait"

    def test_hold_position(self):
        assert parse("hold position").intent == "nav.wait"

    def test_hold_your_position(self):
        assert parse("hold your position").intent == "nav.wait"

    def test_pause_here(self):
        assert parse("pause here").intent == "nav.wait"


class TestNavComeHere:
    def test_come_here(self):
        assert parse("come here").intent == "nav.come_here"

    def test_get_over_here(self):
        assert parse("get over here").intent == "nav.come_here"

    def test_come_to_me(self):
        assert parse("come to me").intent == "nav.come_here"

    def test_approach_me(self):
        assert parse("approach me").intent == "nav.come_here"

    def test_move_towards_me(self):
        assert parse("move towards me").intent == "nav.come_here"


class TestNavPatrol:
    def test_patrol(self):
        assert parse("patrol").intent == "nav.patrol"

    def test_start_patrol(self):
        assert parse("start patrol").intent == "nav.patrol"

    def test_begin_patrol(self):
        assert parse("begin patrol").intent == "nav.patrol"

    def test_go_on_patrol(self):
        assert parse("go on patrol").intent == "nav.patrol"

    def test_start_patrolling(self):
        assert parse("start patrolling").intent == "nav.patrol"

    def test_guard_the_area(self):
        assert parse("guard the area").intent == "nav.patrol"


class TestNavSetMode:
    def test_shadow_me(self):
        r = parse("shadow me")
        assert r.intent == "nav.set_mode"
        assert r.entities.get("mode") == "shadow"

    def test_shadow_mode(self):
        r = parse("shadow mode")
        assert r.intent == "nav.set_mode"
        assert r.entities.get("mode") == "shadow"

    def test_walk_ahead(self):
        r = parse("walk ahead of me")
        assert r.intent == "nav.set_mode"
        assert r.entities.get("mode") == "lead"

    def test_stay_beside_me(self):
        r = parse("stay beside me")
        assert r.intent == "nav.set_mode"
        assert r.entities.get("mode") == "side"

    def test_orbit(self):
        r = parse("orbit")
        assert r.intent == "nav.set_mode"
        assert r.entities.get("mode") == "orbit"

    def test_give_me_space(self):
        r = parse("give me space")
        assert r.intent == "nav.set_mode"
        assert r.entities.get("mode") == "loose"

    def test_stay_close(self):
        r = parse("stay close")
        assert r.intent == "nav.set_mode"
        assert r.entities.get("mode") == "tight"

    def test_change_mode_to(self):
        r = parse("change mode to lead")
        assert r.intent == "nav.set_mode"
        assert r.entities.get("mode") == "lead"

    def test_switch_to_orbit_mode(self):
        r = parse("switch to orbit mode")
        assert r.intent == "nav.set_mode"
        assert r.entities.get("mode") == "orbit"


class TestNavTeachRoute:
    def test_teach_route_name(self):
        r = parse("teach route alpha")
        assert r.intent == "nav.teach_route"
        assert r.entities.get("name") == "alpha"

    def test_record_route(self):
        r = parse("record route morning walk")
        assert r.intent == "nav.teach_route"
        assert r.entities.get("name") == "morning walk"

    def test_remember_this_route(self):
        r = parse("remember this route as perimeter")
        assert r.intent == "nav.teach_route"
        assert r.entities.get("name") == "perimeter"

    def test_start_recording_route(self):
        r = parse("start recording a route")
        assert r.intent == "nav.teach_route"

    def test_learn_route(self):
        r = parse("learn route delta")
        assert r.intent == "nav.teach_route"
        assert r.entities.get("name") == "delta"


class TestNavStopTeaching:
    def test_stop_recording_route(self):
        assert parse("stop recording the route").intent == "nav.stop_teaching"

    def test_stop_teaching_route(self):
        assert parse("stop teaching the route").intent == "nav.stop_teaching"

    def test_finish_recording(self):
        assert parse("finish recording the route").intent == "nav.stop_teaching"

    def test_done_recording(self):
        assert parse("done recording the route").intent == "nav.stop_teaching"


class TestNavReplayRoute:
    def test_replay_route_name(self):
        r = parse("replay route alpha")
        assert r.intent == "nav.replay_route"
        assert r.entities.get("name") == "alpha"

    def test_follow_route_name(self):
        r = parse("follow route beta")
        assert r.intent == "nav.replay_route"
        assert r.entities.get("name") == "beta"

    def test_take_route(self):
        r = parse("take the morning walk route")
        assert r.intent == "nav.replay_route"

    def test_play_back_route(self):
        r = parse("play back route delta")
        assert r.intent == "nav.replay_route"
        assert r.entities.get("name") == "delta"

    def test_go_on_route(self):
        r = parse("go on route perimeter")
        assert r.intent == "nav.replay_route"
        assert r.entities.get("name") == "perimeter"


# ── Social intents ────────────────────────────────────────────────────────────


class TestSocialRememberMe:
    def test_remember_me(self):
        assert parse("remember me").intent == "social.remember_me"

    def test_remember_me_as(self):
        r = parse("remember me as Alice")
        assert r.intent == "social.remember_me"
        assert r.entities.get("name") == "alice"

    def test_my_name_is(self):
        r = parse("my name is Bob")
        assert r.intent == "social.remember_me"
        assert r.entities.get("name") == "bob"

    def test_call_me(self):
        r = parse("call me Charlie")
        assert r.intent == "social.remember_me"
        assert r.entities.get("name") == "charlie"

    def test_remember_my_name(self):
        assert parse("remember my name").intent == "social.remember_me"

    def test_learn_my_name(self):
        assert parse("learn my name").intent == "social.remember_me"

    def test_register_me(self):
        assert parse("register me").intent == "social.remember_me"


class TestSocialForgetMe:
    def test_forget_me(self):
        r = parse("forget me")
        assert r.intent == "social.forget_me"

    def test_delete_me(self):
        assert parse("delete me").intent == "social.forget_me"

    def test_remove_me(self):
        assert parse("remove me").intent == "social.forget_me"

    def test_erase_my_face(self):
        assert parse("erase my face").intent == "social.forget_me"

    def test_requires_confirmation(self):
        assert parse("forget me").requires_confirmation is True

    def test_delete_my_profile(self):
        assert parse("delete my profile").intent == "social.forget_me"


class TestSocialWhatsMyName:
    def test_whats_my_name(self):
        assert parse("what's my name").intent == "social.whats_my_name"

    def test_do_you_know_me(self):
        assert parse("do you know me").intent == "social.whats_my_name"

    def test_who_am_i(self):
        assert parse("who am I").intent == "social.whats_my_name"

    def test_do_you_remember_me(self):
        assert parse("do you remember me").intent == "social.whats_my_name"

    def test_have_you_seen_me_before(self):
        assert parse("have you seen me before").intent == "social.whats_my_name"


class TestSocialTellJoke:
    def test_tell_me_a_joke(self):
        assert parse("tell me a joke").intent == "social.tell_joke"

    def test_say_something_funny(self):
        assert parse("say something funny").intent == "social.tell_joke"

    def test_make_me_laugh(self):
        assert parse("make me laugh").intent == "social.tell_joke"

    def test_a_joke_please(self):
        assert parse("a joke please").intent == "social.tell_joke"

    def test_cheer_me_up(self):
        assert parse("cheer me up").intent == "social.tell_joke"


# ── System intents ────────────────────────────────────────────────────────────


class TestSystemBatteryStatus:
    def test_battery_status(self):
        assert parse("battery status").intent == "system.battery_status"

    def test_battery_level(self):
        assert parse("battery level").intent == "system.battery_status"

    def test_hows_the_battery(self):
        assert parse("how's the battery").intent == "system.battery_status"

    def test_how_much_battery(self):
        assert parse("how much battery").intent == "system.battery_status"

    def test_power_status(self):
        assert parse("power status").intent == "system.battery_status"

    def test_check_battery(self):
        assert parse("check the battery").intent == "system.battery_status"


class TestSystemMapStatus:
    def test_map_status(self):
        assert parse("map status").intent == "system.map_status"

    def test_hows_the_map(self):
        assert parse("how's the map").intent == "system.map_status"

    def test_are_you_mapped(self):
        assert parse("are you mapped").intent == "system.map_status"

    def test_do_you_have_a_map(self):
        assert parse("do you have a map").intent == "system.map_status"


class TestSystemShutdown:
    def test_shutdown(self):
        r = parse("shut down")
        assert r.intent == "system.shutdown"

    def test_shutdown_oneword(self):
        r = parse("shutdown")
        assert r.intent == "system.shutdown"

    def test_power_off(self):
        assert parse("power off").intent == "system.shutdown"

    def test_turn_off(self):
        assert parse("turn off").intent == "system.shutdown"

    def test_requires_confirmation(self):
        assert parse("shut down").requires_confirmation is True

    def test_confidence(self):
        assert parse("power off").confidence == 0.95


class TestSystemRestart:
    def test_restart(self):
        assert parse("restart").intent == "system.restart"

    def test_reboot(self):
        assert parse("reboot").intent == "system.restart"

    def test_start_fresh(self):
        assert parse("start fresh").intent == "system.restart"

    def test_start_over(self):
        assert parse("start over").intent == "system.restart"

    def test_reset_yourself(self):
        assert parse("reset yourself").intent == "system.restart"

    def test_requires_confirmation(self):
        assert parse("restart").requires_confirmation is True


class TestSystemVolumeUp:
    def test_volume_up(self):
        assert parse("volume up").intent == "system.volume_up"

    def test_louder(self):
        assert parse("louder").intent == "system.volume_up"

    def test_turn_up(self):
        assert parse("turn it up").intent == "system.volume_up"

    def test_speak_up(self):
        assert parse("speak up").intent == "system.volume_up"

    def test_increase_volume(self):
        assert parse("increase the volume").intent == "system.volume_up"


class TestSystemVolumeDown:
    def test_volume_down(self):
        assert parse("volume down").intent == "system.volume_down"

    def test_quieter(self):
        assert parse("quieter").intent == "system.volume_down"

    def test_turn_down(self):
        assert parse("turn it down").intent == "system.volume_down"

    def test_decrease_volume(self):
        assert parse("decrease the volume").intent == "system.volume_down"

    def test_mute(self):
        assert parse("mute").intent == "system.volume_down"


class TestSystemVolumeSet:
    def test_set_volume_numeric(self):
        r = parse("set volume to 60")
        assert r.intent == "system.volume_set"
        assert r.entities.get("level") == "60"

    def test_set_volume_percent(self):
        r = parse("set the volume to 80 percent")
        assert r.intent == "system.volume_set"
        assert r.entities.get("level") == "80"

    def test_set_volume_word_low(self):
        r = parse("set volume to low")
        assert r.intent == "system.volume_set"
        assert r.entities.get("level") == "20"

    def test_set_volume_word_high(self):
        r = parse("set the volume to high")
        assert r.intent == "system.volume_set"
        assert r.entities.get("level") == "80"

    def test_volume_to_50(self):
        r = parse("volume to 50")
        assert r.intent == "system.volume_set"
        assert r.entities.get("level") == "50"


# ── Config intents ────────────────────────────────────────────────────────────


class TestConfigPersonality:
    def test_change_personality(self):
        r = parse("change to friendly personality")
        assert r.intent == "config.personality"
        assert r.entities.get("personality") == "friendly"

    def test_switch_personality(self):
        r = parse("switch to formal personality")
        assert r.intent == "config.personality"

    def test_use_personality(self):
        r = parse("use playful personality")
        assert r.intent == "config.personality"
        assert r.entities.get("personality") == "playful"

    def test_be_more_adjective(self):
        r = parse("be more friendly")
        assert r.intent == "config.personality"


class TestConfigSassLevel:
    def test_more_sassy(self):
        assert parse("more sassy").intent == "config.sass_level"

    def test_less_sassy(self):
        assert parse("less sassy").intent == "config.sass_level"

    def test_set_sass_level(self):
        r = parse("set sass level to 7")
        assert r.intent == "config.sass_level"
        assert r.entities.get("level") == "7"

    def test_tone_it_down(self):
        assert parse("tone it down").intent == "config.sass_level"

    def test_less_sarcastic(self):
        assert parse("less sarcastic").intent == "config.sass_level"

    def test_more_witty(self):
        assert parse("more witty").intent == "config.sass_level"


class TestConfigFollowMode:
    def test_change_follow_mode(self):
        r = parse("change follow mode to loose")
        assert r.intent in ("config.follow_mode", "nav.set_mode")

    def test_set_follow_mode(self):
        r = parse("set follow mode to tight")
        assert r.intent in ("config.follow_mode", "nav.set_mode")


# ── Confirmation intents ──────────────────────────────────────────────────────


class TestConfirmYes:
    def test_yes(self):
        assert parse("yes").intent == "confirm.yes"

    def test_yeah(self):
        assert parse("yeah").intent == "confirm.yes"

    def test_yep(self):
        assert parse("yep").intent == "confirm.yes"

    def test_sure(self):
        assert parse("sure").intent == "confirm.yes"

    def test_ok(self):
        assert parse("ok").intent == "confirm.yes"

    def test_go_ahead(self):
        assert parse("go ahead").intent == "confirm.yes"

    def test_do_it(self):
        assert parse("do it").intent == "confirm.yes"

    def test_affirmative(self):
        assert parse("affirmative").intent == "confirm.yes"

    def test_correct(self):
        assert parse("correct").intent == "confirm.yes"


class TestConfirmNo:
    def test_no(self):
        assert parse("no").intent == "confirm.no"

    def test_nope(self):
        assert parse("nope").intent == "confirm.no"

    def test_cancel(self):
        assert parse("cancel").intent == "confirm.no"

    def test_abort(self):
        assert parse("abort").intent == "confirm.no"

    def test_nevermind(self):
        assert parse("never mind").intent == "confirm.no"

    def test_negative(self):
        assert parse("negative").intent == "confirm.no"


# ── Fallback ──────────────────────────────────────────────────────────────────


class TestFallback:
    def test_unrecognised(self):
        r = parse("what is the weather like today")
        assert r.intent == "fallback"
        assert r.confidence == 0.0

    def test_empty_string(self):
        r = parse("")
        assert r.intent == "fallback"

    def test_numbers_only(self):
        r = parse("42")
        assert r.intent == "fallback"

    def test_gibberish(self):
        r = parse("zxqw frp klmn")
        assert r.intent == "fallback"

    def test_partial_match_not_enough(self):
        # "the" alone should not trigger any intent
        r = parse("the")
        assert r.intent == "fallback"

    def test_raw_text_preserved(self):
        r = parse("Hello there!")
        assert "hello there" in r.raw_text


# ── Requires confirmation flag ────────────────────────────────────────────────


class TestRequiresConfirmation:
    def test_shutdown_requires_confirmation(self):
        assert parse("shut down").requires_confirmation is True

    def test_restart_requires_confirmation(self):
        assert parse("restart").requires_confirmation is True

    def test_forget_me_requires_confirmation(self):
        assert parse("forget me").requires_confirmation is True

    def test_stop_does_not_require_confirmation(self):
        assert parse("stop").requires_confirmation is False

    def test_go_home_does_not_require_confirmation(self):
        assert parse("go home").requires_confirmation is False

    def test_battery_does_not_require_confirmation(self):
        assert parse("battery status").requires_confirmation is False


# ── Confidence values ─────────────────────────────────────────────────────────


class TestConfidence:
    def test_high_confidence_intents(self):
        for text in ("go to the kitchen", "stop", "shut down", "forget me"):
            r = parse(text)
            assert r.confidence >= 0.90, f"Low confidence for: {text!r}"

    def test_fallback_has_zero_confidence(self):
        assert parse("what is two plus two").confidence == 0.0


# ── Case insensitivity ────────────────────────────────────────────────────────


class TestCaseInsensitivity:
    def test_uppercase(self):
        assert parse("STOP").intent == "nav.stop"

    def test_mixed_case(self):
        assert parse("Go To The Kitchen").intent == "nav.go_to"

    def test_upper_follow_me(self):
        assert parse("FOLLOW ME").intent == "nav.follow_me"


# ── Mode inference from text ──────────────────────────────────────────────────


class TestModeFromText:
    def test_shadow_keyword(self):
        assert _mode_from_text("shadow") == "shadow"

    def test_lead_keyword(self):
        assert _mode_from_text("go ahead") == "lead"

    def test_orbit_keyword(self):
        assert _mode_from_text("orbit") == "orbit"

    def test_none_for_unknown(self):
        assert _mode_from_text("random text here") is None
