"""Tests for the unified persona system prompt.

The persona should match the user's configured wake word so renaming the
wake word to e.g. "Friday" produces a butler named Friday, not one still
hardcoded to Jarvis.
"""

from jarvis.system_prompt import (
    build_system_prompt,
    load_agent_instructions,
    output_language_directive,
    reply_language_directive,
    ENGLISH_ONLY_DIRECTIVE,
)


class TestBuildSystemPrompt:
    def test_default_name_is_jarvis(self):
        prompt = build_system_prompt()
        assert "named Jarvis" in prompt

    def test_custom_name_replaces_jarvis(self):
        prompt = build_system_prompt("Friday")
        assert "named Friday" in prompt
        assert "named Jarvis" not in prompt

    def test_lowercase_wake_word_is_capitalised(self):
        prompt = build_system_prompt("friday".capitalize())
        assert "named Friday" in prompt

    def test_blank_name_falls_back_to_jarvis(self):
        assert "named Jarvis" in build_system_prompt("")
        assert "named Jarvis" in build_system_prompt("   ")
        assert "named Jarvis" in build_system_prompt(None)  # type: ignore[arg-type]

    def test_default_keeps_user_language_clause(self):
        # Without a lock, the persona still mirrors the user's language.
        assert "in the user's language" in build_system_prompt("Jarvis")

    def test_language_lock_rewrites_user_language_clause(self):
        # With a lock, the contradicting "user's language" clause is rewritten
        # so the persona does not fight the OUTPUT_LANGUAGE directive.
        prompt = build_system_prompt("Jarvis", "Korean")
        assert "in the user's language" not in prompt
        assert "in Korean" in prompt

    def test_persona_enforces_single_sentence(self):
        # Spoken replies must be one sentence (TTS latency scales with text
        # length, and the user asked for one-sentence answers). The persona must
        # state the single-sentence rule and must NOT carry the old "two at the
        # very most" allowance that let the model run long.
        prompt = build_system_prompt("Jarvis")
        assert "single short sentence" in prompt
        assert "Never write a second sentence" in prompt
        assert "two at the very most" not in prompt

    def test_persona_aside_does_not_authorise_a_second_sentence(self):
        # The dry aside must fold into the one sentence, not become a 2nd one.
        prompt = build_system_prompt("Jarvis")
        assert "SINGLE sentence" in prompt
        assert "never add it as " in prompt

    def test_persona_has_no_copyable_proper_noun_examples(self):
        # A weak model parroted the literal "Trenches Gym" example from the
        # persona as if it were a real user fact (boxing mangled to tennis).
        # The persona must not embed copyable personal proper nouns, and must
        # tell the model to use ONLY facts that literally appear in the memory
        # section — never borrow names/places from the instructions themselves.
        prompt = build_system_prompt("Jarvis")
        assert "Trenches" not in prompt
        assert "never borrow any name, place, or activity from these instructions" in prompt


class TestOutputLanguageDirective:
    """A deployment may lock replies to a single language via OUTPUT_LANGUAGE.

    Unset (the default) must keep the assistant's multilingual behaviour of
    replying in the user's own language, so the helper returns None and no
    directive is injected.
    """

    def test_unset_returns_none(self):
        assert output_language_directive(None) is None
        assert output_language_directive("") is None
        assert output_language_directive("   ") is None

    def test_set_language_is_named_and_exclusive(self):
        directive = output_language_directive("Korean")
        assert directive is not None
        assert "Korean" in directive
        # Must force exclusivity, not merely prefer the language.
        assert "only" in directive.lower()

    def test_language_agnostic(self):
        # The helper takes any language string — no hardcoded single language.
        assert "French" in (output_language_directive("French") or "")
        assert "日本語" in (output_language_directive("日本語") or "")

    def test_strips_surrounding_whitespace(self):
        directive = output_language_directive("  Korean  ")
        assert directive is not None
        assert "Korean" in directive
        assert "  Korean" not in directive


class TestReplyLanguageDirective:
    """Precedence: explicit OUTPUT_LANGUAGE lock > English-only TTS > free.

    The lock must override the Piper/Chatterbox English fallback, because a
    deployment that sets OUTPUT_LANGUAGE (e.g. Korean) also runs a TTS voice
    that can speak it. Without this, the English lock and the Korean lock
    contradict each other and the model reverts to English.
    """

    def test_lock_overrides_english_only_tts(self):
        directive = reply_language_directive("Korean", "piper")
        assert directive is not None
        assert "Korean" in directive
        assert directive != ENGLISH_ONLY_DIRECTIVE

    def test_english_only_tts_forces_english_without_lock(self):
        assert reply_language_directive(None, "piper") == ENGLISH_ONLY_DIRECTIVE
        assert reply_language_directive("", "chatterbox") == ENGLISH_ONLY_DIRECTIVE

    def test_no_lock_multilingual_tts_is_free(self):
        # A non-English-only engine (e.g. melo) with no lock → reply in the
        # user's own language, so no directive.
        assert reply_language_directive(None, "melo") is None

    def test_unknown_tts_defaults_to_english_only(self):
        # Preserves the original getattr(cfg, 'tts_engine', 'piper') default:
        # an unknown/missing engine is treated conservatively as English-only.
        assert reply_language_directive(None, None) == ENGLISH_ONLY_DIRECTIVE

    def test_lock_wins_even_with_multilingual_tts(self):
        directive = reply_language_directive("Korean", "melo")
        assert directive is not None and "Korean" in directive

    def test_edge_is_multilingual(self):
        # Edge TTS (the default Korean voice) is not English-only: no lock → the
        # user's own language, and a lock is honoured (not forced to English).
        assert reply_language_directive(None, "edge") is None
        directive = reply_language_directive("Korean", "edge")
        assert directive is not None and "Korean" in directive
        assert directive != ENGLISH_ONLY_DIRECTIVE


class TestLoadAgentInstructions:
    """Operator can extend the reply LLM's system prompt by dropping *.md files
    into an agents/ folder. The loader concatenates them in filename order and
    fails open so a missing/empty/broken folder never breaks a reply."""

    def test_missing_dir_returns_empty(self, tmp_path):
        assert load_agent_instructions(str(tmp_path / "does-not-exist")) == ""

    def test_empty_dir_returns_empty(self, tmp_path):
        assert load_agent_instructions(str(tmp_path)) == ""

    def test_reads_and_concatenates_single_file(self, tmp_path):
        (tmp_path / "rules.md").write_text("Always be brief.", encoding="utf-8")
        assert load_agent_instructions(str(tmp_path)) == "Always be brief."

    def test_files_are_ordered_by_filename(self, tmp_path):
        # Filename prefixes let the operator control ordering.
        (tmp_path / "10-second.md").write_text("SECOND", encoding="utf-8")
        (tmp_path / "00-first.md").write_text("FIRST", encoding="utf-8")
        result = load_agent_instructions(str(tmp_path))
        assert result.index("FIRST") < result.index("SECOND")

    def test_only_md_files_are_read(self, tmp_path):
        (tmp_path / "note.txt").write_text("IGNORE ME", encoding="utf-8")
        (tmp_path / "use.md").write_text("USE ME", encoding="utf-8")
        result = load_agent_instructions(str(tmp_path))
        assert "USE ME" in result
        assert "IGNORE ME" not in result

    def test_blank_files_are_skipped(self, tmp_path):
        (tmp_path / "blank.md").write_text("   \n  ", encoding="utf-8")
        (tmp_path / "real.md").write_text("Real instruction.", encoding="utf-8")
        assert load_agent_instructions(str(tmp_path)) == "Real instruction."

    def test_env_var_is_used_when_no_arg(self, tmp_path, monkeypatch):
        (tmp_path / "a.md").write_text("FROM ENV", encoding="utf-8")
        monkeypatch.setenv("AGENTS_DIR", str(tmp_path))
        assert load_agent_instructions() == "FROM ENV"

    def test_explicit_arg_overrides_env(self, tmp_path, monkeypatch):
        (tmp_path / "env.md").write_text("ENV", encoding="utf-8")
        other = tmp_path / "other"
        other.mkdir()
        (other / "arg.md").write_text("ARG", encoding="utf-8")
        monkeypatch.setenv("AGENTS_DIR", str(tmp_path))
        assert load_agent_instructions(str(other)) == "ARG"

    def test_a_file_path_instead_of_dir_returns_empty(self, tmp_path):
        f = tmp_path / "file.md"
        f.write_text("x", encoding="utf-8")
        # Pointed at a file, not a directory → fail-open.
        assert load_agent_instructions(str(f)) == ""
