"""Tests for format_conversation (v2) in cortex/extraction/prompts_v2.py.

Covers role remapping (speaker_a/speaker_b), session_date header,
empty/single-message conversations, missing/unexpected role values,
and content edge cases.

Issue: #1305
"""

import pytest

from cortex.extraction.prompts_v2 import format_conversation


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _msgs(*pairs):
    """Shorthand: _msgs(("user", "hi"), ("assistant", "hello"))"""
    return [{"role": role, "content": content} for role, content in pairs]


# ---------------------------------------------------------------------------
# Basic role mapping (no speaker overrides)
# ---------------------------------------------------------------------------

class TestBasicRoleMapping:
    """Roles should be uppercased in output when no speaker names given."""

    def test_user_and_assistant(self):
        out = format_conversation(_msgs(("user", "hi"), ("assistant", "hello")))
        assert "USER: hi" in out
        assert "ASSISTANT: hello" in out

    def test_roles_are_uppercased(self):
        out = format_conversation(_msgs(("user", "hey")))
        assert "USER: hey" in out

    def test_unknown_role_preserved(self):
        out = format_conversation([{"role": "system", "content": "sys msg"}])
        assert "SYSTEM: sys msg" in out


# ---------------------------------------------------------------------------
# Speaker remapping (speaker_a → USER, speaker_b → ASSISTANT)
# ---------------------------------------------------------------------------

class TestSpeakerRemapping:
    """When speaker_a/speaker_b are provided, USER→speaker_a, ASSISTANT→speaker_b."""

    def test_both_speakers(self):
        out = format_conversation(
            _msgs(("user", "hi"), ("assistant", "hello")),
            speaker_a="Andrew",
            speaker_b="Eva",
        )
        assert "ANDREW: hi" in out
        assert "EVA: hello" in out
        assert "USER" not in out
        assert "ASSISTANT" not in out

    def test_speaker_a_only(self):
        """speaker_a alone remaps USER→speaker_a; ASSISTANT stays as-is."""
        out = format_conversation(
            _msgs(("user", "hi"), ("assistant", "hello")),
            speaker_a="Andrew",
        )
        assert "ANDREW: hi" in out
        assert "ASSISTANT: hello" in out

    def test_speaker_b_only(self):
        """speaker_b alone shouldn't trigger speaker header or remapping (needs both)."""
        out = format_conversation(
            _msgs(("user", "hi"), ("assistant", "hello")),
            speaker_b="Eva",
        )
        # speaker_b without speaker_a → still remaps ASSISTANT role
        # (the role remapping checks speaker_b independently of the header)
        assert "EVA: hello" in out

    def test_speaker_names_case_insensitive_input(self):
        """Speaker names are uppercased regardless of input case."""
        out = format_conversation(
            _msgs(("assistant", "yo")),
            speaker_a="alice",
            speaker_b="bob",
        )
        assert "BOB: yo" in out

    def test_non_user_assistant_roles_not_remapped(self):
        """Only USER and ASSISTANT roles get remapped, others pass through."""
        out = format_conversation(
            [{"role": "system", "content": "sys"}],
            speaker_a="Andrew",
            speaker_b="Eva",
        )
        assert "SYSTEM: sys" in out


# ---------------------------------------------------------------------------
# All speaker_a / speaker_b combinations
# ---------------------------------------------------------------------------

class TestSpeakerCombinations:
    """Exhaustive combinations of speaker_a and speaker_b presence."""

    @pytest.mark.parametrize(
        "speaker_a, speaker_b, expect_user_label, expect_asst_label",
        [
            (None, None, "USER", "ASSISTANT"),
            ("Andrew", None, "ANDREW", "ASSISTANT"),  # speaker_a remaps USER independently
            (None, "Eva", "USER", "EVA"),  # speaker_b remaps assistant independently
            ("Andrew", "Eva", "ANDREW", "EVA"),
        ],
        ids=["none-none", "a-only", "b-only", "both"],
    )
    def test_label_combinations(self, speaker_a, speaker_b, expect_user_label, expect_asst_label):
        out = format_conversation(
            _msgs(("user", "msg1"), ("assistant", "msg2")),
            speaker_a=speaker_a,
            speaker_b=speaker_b,
        )
        assert f"{expect_user_label}: msg1" in out
        assert f"{expect_asst_label}: msg2" in out


# ---------------------------------------------------------------------------
# Session date header
# ---------------------------------------------------------------------------

class TestSessionDateHeader:
    """session_date should appear as a context header."""

    def test_date_included(self):
        out = format_conversation(
            _msgs(("user", "hi")),
            session_date="1:56 pm on 8 May, 2023",
        )
        assert "[Session date: 1:56 pm on 8 May, 2023]" in out

    def test_no_date_no_header(self):
        out = format_conversation(_msgs(("user", "hi")))
        assert "[Session date:" not in out

    def test_speakers_header(self):
        out = format_conversation(
            _msgs(("user", "hi")),
            speaker_a="Andrew",
            speaker_b="Eva",
        )
        assert "[Speakers: Andrew and Eva]" in out

    def test_date_and_speakers_header(self):
        out = format_conversation(
            _msgs(("user", "hi")),
            session_date="May 8",
            speaker_a="Andrew",
            speaker_b="Eva",
        )
        assert "[Session date: May 8]" in out
        assert "[Speakers: Andrew and Eva]" in out


# ---------------------------------------------------------------------------
# Empty and single-message conversations
# ---------------------------------------------------------------------------

class TestEdgeCases:
    """Edge cases: empty list, single message, whitespace-only content."""

    def test_empty_conversation(self):
        out = format_conversation([])
        assert out == ""

    def test_single_message(self):
        out = format_conversation(_msgs(("user", "only one")))
        assert "USER: only one" in out

    def test_whitespace_only_content_skipped(self):
        out = format_conversation([{"role": "user", "content": "   "}])
        assert out.strip() == ""

    def test_none_content_skipped(self):
        out = format_conversation([{"role": "user", "content": None}])
        assert out.strip() == ""

    def test_empty_string_content_skipped(self):
        out = format_conversation([{"role": "user", "content": ""}])
        assert out.strip() == ""

    def test_mixed_empty_and_real(self):
        msgs = [
            {"role": "user", "content": ""},
            {"role": "user", "content": "real message"},
            {"role": "assistant", "content": "   "},
        ]
        out = format_conversation(msgs)
        assert "real message" in out
        # empty/whitespace ones should not produce role labels
        lines = [l for l in out.split("\n") if l.strip()]
        assert len(lines) == 1


# ---------------------------------------------------------------------------
# Missing / unexpected role values
# ---------------------------------------------------------------------------

class TestMissingAndUnexpectedRoles:
    """Messages with no role field or unexpected role values."""

    def test_missing_role_defaults_to_unknown(self):
        out = format_conversation([{"content": "no role here"}])
        assert "UNKNOWN: no role here" in out

    def test_missing_content_key(self):
        """Message with role but no content key → skipped (empty content)."""
        out = format_conversation([{"role": "user"}])
        assert out.strip() == ""

    def test_completely_empty_dict(self):
        out = format_conversation([{}])
        assert out.strip() == ""

    def test_unexpected_role_value(self):
        """Arbitrary role strings should be uppercased and passed through."""
        out = format_conversation([{"role": "moderator", "content": "order!"}])
        assert "MODERATOR: order!" in out

    def test_numeric_role(self):
        """Non-string role — get() returns it, .upper() may fail but we test behavior."""
        # The function calls .upper() on the role, so non-strings would error.
        # This tests that the function expects string roles.
        with pytest.raises(AttributeError):
            format_conversation([{"role": 123, "content": "bad"}])


# ---------------------------------------------------------------------------
# extract_assistant_turns (pipeline-level, not format_conversation itself)
# ---------------------------------------------------------------------------

class TestExtractAssistantTurns:
    """extract_assistant_turns filtering happens in the pipeline, not in
    format_conversation. But we verify format_conversation correctly
    includes/excludes assistant messages based on what's passed in.
    
    The actual filtering is:
        if not self.extract_assistant_turns:
            filtered = [m for m in filtered if m.get("role", "user") != "assistant"]
    """

    def test_with_assistant_turns_included(self):
        """When assistant messages ARE in the list, they appear in output."""
        msgs = _msgs(("user", "q"), ("assistant", "a"), ("user", "q2"))
        out = format_conversation(msgs)
        assert "ASSISTANT: a" in out

    def test_without_assistant_turns(self):
        """Simulating extract_assistant_turns=False: pre-filter assistant out."""
        msgs = _msgs(("user", "q"), ("assistant", "a"), ("user", "q2"))
        filtered = [m for m in msgs if m.get("role") != "assistant"]
        out = format_conversation(filtered)
        assert "ASSISTANT" not in out
        assert "USER: q" in out
        assert "USER: q2" in out

    def test_all_assistant_filtered_yields_empty(self):
        """If all messages are assistant and filtered out → empty input."""
        msgs = _msgs(("assistant", "a1"), ("assistant", "a2"))
        filtered = [m for m in msgs if m.get("role") != "assistant"]
        out = format_conversation(filtered)
        assert out == ""


# ---------------------------------------------------------------------------
# Output format / structure
# ---------------------------------------------------------------------------

class TestOutputStructure:
    """Verify the output format: blank lines between turns, trailing whitespace."""

    def test_blank_lines_between_turns(self):
        out = format_conversation(_msgs(("user", "a"), ("assistant", "b")))
        # Each turn should be followed by a blank line (except trailing is stripped)
        assert "USER: a\n\nASSISTANT: b" in out

    def test_no_trailing_whitespace(self):
        out = format_conversation(_msgs(("user", "hi")))
        assert out == out.rstrip()

    def test_header_separated_by_blank_line(self):
        out = format_conversation(
            _msgs(("user", "hi")),
            session_date="May 8",
        )
        lines = out.split("\n")
        assert lines[0].startswith("[Session date:")
        assert lines[1] == ""  # blank separator
        assert "USER: hi" in lines[2]
