# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

"""Tests for the SeedGroup and AttackSeedGroup classes."""

import uuid

import pytest

from pyrit.models.seeds import (
    AttackSeedGroup,
    AttackTechniqueSeedGroup,
    SeedGroup,
    SeedObjective,
    SeedPrompt,
    SeedSimulatedConversation,
)

# =============================================================================
# SeedGroup Tests
# =============================================================================


class TestSeedGroupInit:
    """Tests for SeedGroup initialization."""

    def test_init_with_single_prompt(self):
        """Test initialization with a single SeedPrompt."""
        prompt = SeedPrompt(value="Test prompt", data_type="text")
        group = SeedGroup(seeds=[prompt])

        assert len(group.seeds) == 1
        assert group.prompts[0].value == "Test prompt"

    def test_init_with_multiple_prompts(self):
        """Test initialization with multiple SeedPrompts."""
        prompts = [
            SeedPrompt(value="Prompt 1", data_type="text", sequence=0, role="user"),
            SeedPrompt(value="Prompt 2", data_type="text", sequence=1, role="assistant"),
        ]
        group = SeedGroup(seeds=prompts)

        assert len(group.seeds) == 2
        assert len(group.prompts) == 2

    def test_init_with_objective_and_prompts(self):
        """Test initialization with objective and prompts."""
        objective = SeedObjective(value="Test objective")
        prompt = SeedPrompt(value="Test prompt", data_type="text")
        group = SeedGroup(seeds=[objective, prompt])

        assert len(group.seeds) == 2
        # Objective should be first
        assert isinstance(group.seeds[0], SeedObjective)

    def test_init_with_dict_seeds(self):
        """Test initialization with dictionary seeds."""
        group = SeedGroup(
            seeds=[
                {"value": "Test objective", "seed_type": "objective"},
                {"value": "Test prompt", "data_type": "text"},
            ]
        )

        assert len(group.seeds) == 2
        assert isinstance(group.seeds[0], SeedObjective)
        assert isinstance(group.seeds[1], SeedPrompt)

    def test_init_empty_raises_error(self):
        """Test that empty seeds raises ValueError."""
        with pytest.raises(ValueError, match="SeedGroup cannot be empty"):
            SeedGroup(seeds=[])

    def test_init_multiple_objectives_raises_error(self):
        """Test that multiple objectives raises ValueError."""
        with pytest.raises(ValueError, match="SeedGroup can only have one objective"):
            SeedGroup(
                seeds=[
                    SeedObjective(value="Objective 1"),
                    SeedObjective(value="Objective 2"),
                ]
            )

    def test_init_assigns_consistent_group_id(self):
        """Test that all seeds get the same prompt_group_id."""
        prompts = [
            SeedPrompt(value="Prompt 1", data_type="text"),
            SeedPrompt(value="Prompt 2", data_type="text"),
        ]
        group = SeedGroup(seeds=prompts)

        group_ids = {seed.prompt_group_id for seed in group.seeds}
        assert len(group_ids) == 1
        assert None not in group_ids

    def test_init_preserves_existing_group_id(self):
        """Test that existing group_id is preserved."""
        existing_id = uuid.uuid4()
        prompts = [
            SeedPrompt(value="Prompt 1", data_type="text", prompt_group_id=existing_id),
            SeedPrompt(value="Prompt 2", data_type="text"),
        ]
        group = SeedGroup(seeds=prompts)

        for seed in group.seeds:
            assert seed.prompt_group_id == existing_id

    def test_init_sorts_prompts_by_sequence(self):
        """Test that prompts are sorted by sequence."""
        prompts = [
            SeedPrompt(value="Prompt 2", data_type="text", sequence=2, role="user"),
            SeedPrompt(value="Prompt 0", data_type="text", sequence=0, role="user"),
            SeedPrompt(value="Prompt 1", data_type="text", sequence=1, role="assistant"),
        ]
        group = SeedGroup(seeds=prompts)

        # Check prompts are in order
        assert group.prompts[0].value == "Prompt 0"
        assert group.prompts[1].value == "Prompt 1"
        assert group.prompts[2].value == "Prompt 2"

    def test_init_objective_first_then_sorted_prompts(self):
        """Test that objective comes first, then sorted prompts."""
        seeds = [
            SeedPrompt(value="Prompt 2", data_type="text", sequence=2, role="user"),
            SeedObjective(value="Objective"),
            SeedPrompt(value="Prompt 0", data_type="text", sequence=0, role="assistant"),
        ]
        group = SeedGroup(seeds=seeds)

        assert isinstance(group.seeds[0], SeedObjective)
        assert group.seeds[1].value == "Prompt 0"
        assert group.seeds[2].value == "Prompt 2"


class TestSeedGroupHarmCategories:
    """Tests for SeedGroup.harm_categories property."""

    def test_harm_categories_empty(self):
        """Test harm_categories with no categories."""
        prompt = SeedPrompt(value="Test", data_type="text")
        group = SeedGroup(seeds=[prompt])

        assert group.harm_categories == []

    def test_harm_categories_from_single_seed(self):
        """Test harm_categories from a single seed."""
        prompt = SeedPrompt(
            value="Test",
            data_type="text",
            harm_categories=["violence", "hate"],
        )
        group = SeedGroup(seeds=[prompt])

        assert set(group.harm_categories) == {"violence", "hate"}

    def test_harm_categories_deduplicated(self):
        """Test that harm_categories are deduplicated."""
        prompts = [
            SeedPrompt(value="Test 1", data_type="text", harm_categories=["violence"]),
            SeedPrompt(value="Test 2", data_type="text", harm_categories=["violence", "hate"]),
        ]
        group = SeedGroup(seeds=prompts)

        assert set(group.harm_categories) == {"violence", "hate"}


# =============================================================================
# AttackSeedGroup Tests
# =============================================================================


class TestAttackSeedGroupInit:
    """Tests for AttackSeedGroup initialization."""

    def test_init_with_objective_and_prompt(self):
        """Test basic initialization with objective and prompt."""
        group = AttackSeedGroup(
            seeds=[
                SeedObjective(value="Test objective"),
                SeedPrompt(value="Test prompt", data_type="text"),
            ]
        )

        assert group.objective is not None
        assert group.objective.value == "Test objective"
        assert len(group.prompts) == 1

    def test_init_with_simulated_conversation(self, tmp_path):
        """Test initialization with simulated conversation config."""
        adv_path = tmp_path / "adversarial.yaml"
        adv_path.write_text("value: Adversarial\ndata_type: text")

        group = AttackSeedGroup(
            seeds=[
                SeedObjective(value="Test objective"),
                SeedSimulatedConversation(
                    num_turns=3,
                    adversarial_chat_system_prompt_path=adv_path,
                ),
            ]
        )

        assert group.has_simulated_conversation
        assert group.simulated_conversation_config is not None
        assert group.simulated_conversation_config.num_turns == 3

    def test_init_with_dict_simulated_conversation(self, tmp_path):
        """Test initialization with dict-based simulated conversation."""
        adv_path = tmp_path / "adversarial.yaml"
        adv_path.write_text("value: Adversarial\ndata_type: text")

        group = AttackSeedGroup(
            seeds=[
                {"value": "Test objective", "seed_type": "objective"},
                {
                    "seed_type": "simulated_conversation",
                    "num_turns": 5,
                    "adversarial_chat_system_prompt_path": str(adv_path),
                },
            ]
        )

        assert group.has_simulated_conversation
        assert group.simulated_conversation_config.num_turns == 5

    def test_init_simulated_conversation_with_overlapping_prompts_raises_error(self, tmp_path):
        """Test that simulated_conversation with overlapping prompt sequences raises error."""
        adv_path = tmp_path / "adversarial.yaml"
        adv_path.write_text("value: Adversarial\ndata_type: text")

        # SeedSimulatedConversation with sequence=0 and num_turns=3 occupies sequences 0-5
        # SeedPrompt with sequence=2 overlaps with that range
        with pytest.raises(ValueError, match="overlaps with SeedSimulatedConversation"):
            AttackSeedGroup(
                seeds=[
                    SeedObjective(value="Objective"),
                    SeedSimulatedConversation(
                        num_turns=3,
                        adversarial_chat_system_prompt_path=adv_path,
                        sequence=0,
                    ),
                    SeedPrompt(value="Prompt 1", data_type="text", sequence=2, role="user"),
                ]
            )

    def test_init_ordering_objective_simulated(self, tmp_path):
        """Test that seeds are ordered: objective, simulated_conversation."""
        adv_path = tmp_path / "adversarial.yaml"
        adv_path.write_text("value: adv\ndata_type: text")

        group = AttackSeedGroup(
            seeds=[
                {
                    "seed_type": "simulated_conversation",
                    "num_turns": 2,
                    "adversarial_chat_system_prompt_path": str(adv_path),
                },
                {"value": "Objective", "seed_type": "objective"},
            ]
        )

        assert isinstance(group.seeds[0], SeedObjective)
        assert isinstance(group.seeds[1], SeedSimulatedConversation)


class TestAttackSeedGroupObjective:
    """Tests for AttackSeedGroup objective handling."""

    def test_objective_property_returns_objective(self):
        """Test that objective property returns the SeedObjective."""
        group = AttackSeedGroup(
            seeds=[
                SeedObjective(value="My objective"),
                SeedPrompt(value="Test", data_type="text"),
            ]
        )

        assert group.objective.value == "My objective"

    def test_no_objective_raises_error(self):
        """Test that AttackSeedGroup without objective raises error."""
        with pytest.raises(ValueError, match="must have exactly one objective"):
            AttackSeedGroup(seeds=[SeedPrompt(value="Test", data_type="text")])

    def test_objective_value_can_be_updated(self):
        """Test that objective value can be updated directly."""
        group = AttackSeedGroup(
            seeds=[
                SeedObjective(value="Old objective"),
                SeedPrompt(value="Test", data_type="text"),
            ]
        )

        group.objective.value = "New objective"

        assert group.objective.value == "New objective"


class TestAttackSeedGroupSimulatedConversation:
    """Tests for AttackSeedGroup simulated conversation handling."""

    def test_has_simulated_conversation_false_when_none(self):
        """Test has_simulated_conversation is False when no config."""
        group = AttackSeedGroup(
            seeds=[
                SeedObjective(value="Objective"),
                SeedPrompt(value="Test", data_type="text"),
            ]
        )

        assert not group.has_simulated_conversation

    def test_has_simulated_conversation_true_when_present(self, tmp_path):
        """Test has_simulated_conversation is True when config present."""
        adv_path = tmp_path / "adversarial.yaml"
        adv_path.write_text("value: Adversarial\ndata_type: text")

        group = AttackSeedGroup(
            seeds=[
                SeedObjective(value="Objective"),
                SeedSimulatedConversation(
                    num_turns=3,
                    adversarial_chat_system_prompt_path=adv_path,
                ),
            ]
        )

        assert group.has_simulated_conversation

    def test_simulated_conversation_allows_non_overlapping_prompts(self, tmp_path):
        """Test that prompts can coexist with simulated conversation if sequences don't overlap."""
        adv_path = tmp_path / "adversarial.yaml"
        adv_path.write_text("value: Adversarial\ndata_type: text")

        # SeedSimulatedConversation with sequence=0 and num_turns=2 occupies sequences 0-3 (2*2=4)
        # A prompt with sequence=10 does NOT overlap
        group = AttackSeedGroup(
            seeds=[
                SeedObjective(value="Objective"),
                SeedSimulatedConversation(
                    num_turns=2,
                    adversarial_chat_system_prompt_path=adv_path,
                    sequence=0,
                ),
                SeedPrompt(value="Static follow-up", data_type="text", sequence=10, role="user"),
            ]
        )

        assert group.has_simulated_conversation
        assert len(group.prompts) == 1
        assert group.prompts[0].value == "Static follow-up"

    def test_simulated_conversation_with_custom_sequence(self, tmp_path):
        """Test simulated conversation with non-zero sequence allows prompts before it."""
        adv_path = tmp_path / "adversarial.yaml"
        adv_path.write_text("value: Adversarial\ndata_type: text")

        # SeedSimulatedConversation with sequence=5 and num_turns=2 occupies sequences 5-8
        # A prompt with sequence=0 does NOT overlap (it's before the simulated range)
        group = AttackSeedGroup(
            seeds=[
                SeedObjective(value="Objective"),
                SeedPrompt(value="Static intro", data_type="text", sequence=0, role="user"),
                SeedSimulatedConversation(
                    num_turns=2,
                    adversarial_chat_system_prompt_path=adv_path,
                    sequence=5,
                ),
            ]
        )

        assert group.has_simulated_conversation
        assert len(group.prompts) == 1
        assert group.prompts[0].value == "Static intro"


class TestAttackSeedGroupMessageExtraction:
    """Tests for AttackSeedGroup message extraction methods."""

    def test_is_single_turn_false_for_attack_group_with_objective(self):
        """Test is_single_turn is False for AttackSeedGroup (always has objective)."""
        group = AttackSeedGroup(
            seeds=[
                SeedObjective(value="Objective"),
                SeedPrompt(value="Test", data_type="text"),
            ]
        )

        # AttackSeedGroup always has objective, so is_single_turn is always False
        assert not group.is_single_turn()

    def test_is_single_turn_false_with_objective(self):
        """Test is_single_turn is False when objective present."""
        group = AttackSeedGroup(
            seeds=[
                SeedObjective(value="Objective"),
                SeedPrompt(value="Test", data_type="text"),
            ]
        )

        assert not group.is_single_turn()

    def test_is_single_request_true_for_single_sequence(self):
        """Test is_single_request is True for single sequence."""
        group = AttackSeedGroup(
            seeds=[
                SeedObjective(value="Objective"),
                SeedPrompt(value="Test 1", data_type="text", sequence=0, role="user"),
                SeedPrompt(value="Test 2", data_type="text", sequence=0, role="user"),
            ]
        )

        assert group.is_single_request()

    def test_is_single_request_false_for_multi_sequence(self):
        """Test is_single_request is False for multi-sequence."""
        group = AttackSeedGroup(
            seeds=[
                SeedObjective(value="Objective"),
                SeedPrompt(value="Test 1", data_type="text", sequence=0, role="user"),
                SeedPrompt(value="Test 2", data_type="text", sequence=1, role="assistant"),
            ]
        )

        assert not group.is_single_request()

    def test_next_message_returns_last_user_message(self):
        """Test next_message returns the last user message."""
        group = AttackSeedGroup(
            seeds=[
                SeedObjective(value="Objective"),
                SeedPrompt(value="Test prompt", data_type="text", role="user"),
            ]
        )

        next_msg = group.next_message
        assert next_msg is not None
        assert next_msg.get_value() == "Test prompt"

    def test_next_message_none_for_assistant_last(self):
        """Test next_message is None when last message is assistant."""
        group = AttackSeedGroup(
            seeds=[
                SeedObjective(value="Objective"),
                SeedPrompt(value="User msg", data_type="text", sequence=0, role="user"),
                SeedPrompt(value="Assistant msg", data_type="text", sequence=1, role="assistant"),
            ]
        )

        assert group.next_message is None

    def test_prepended_conversation_returns_all_except_last_user(self):
        """Test prepended_conversation returns all except last user message."""
        group = AttackSeedGroup(
            seeds=[
                SeedObjective(value="Objective"),
                SeedPrompt(value="User 1", data_type="text", sequence=0, role="user"),
                SeedPrompt(value="Assistant 1", data_type="text", sequence=1, role="assistant"),
                SeedPrompt(value="User 2", data_type="text", sequence=2, role="user"),
            ]
        )

        prepended = group.prepended_conversation
        assert prepended is not None
        assert len(prepended) == 2

    def test_user_messages_returns_all_prompts_as_messages(self):
        """Test user_messages returns all prompts as Messages."""
        group = AttackSeedGroup(
            seeds=[
                SeedObjective(value="Objective"),
                SeedPrompt(value="Prompt 1", data_type="text", sequence=0, role="user"),
                SeedPrompt(value="Prompt 2", data_type="text", sequence=1, role="assistant"),
            ]
        )

        messages = group.user_messages
        assert len(messages) == 2


class TestAttackSeedGroupRepr:
    """Tests for AttackSeedGroup.__repr__ method."""

    def test_repr_basic(self):
        """Test basic __repr__ output."""
        group = AttackSeedGroup(
            seeds=[
                SeedObjective(value="Objective"),
                SeedPrompt(value="Test", data_type="text"),
            ]
        )

        repr_str = repr(group)
        assert "SeedGroup" in repr_str
        assert "seeds=" in repr_str

    def test_repr_with_simulated_conversation(self, tmp_path):
        """Test __repr__ includes simulated indicator."""
        adv_path = tmp_path / "adversarial.yaml"
        adv_path.write_text("value: Adversarial\ndata_type: text")

        group = AttackSeedGroup(
            seeds=[
                SeedObjective(value="Objective"),
                SeedSimulatedConversation(
                    num_turns=3,
                    adversarial_chat_system_prompt_path=adv_path,
                ),
            ]
        )

        repr_str = repr(group)
        assert "simulated" in repr_str


# =============================================================================
# AttackSeedGroup.with_technique Tests
# =============================================================================


class TestAttackSeedGroupWithTechnique:
    """Tests for AttackSeedGroup.with_technique() method."""

    def _make_base_group(self) -> AttackSeedGroup:
        return AttackSeedGroup(
            seeds=[
                SeedObjective(value="objective"),
                SeedPrompt(value="prompt1", data_type="text"),
            ]
        )

    def _make_technique(self, *, insertion_index: int | None = None) -> AttackTechniqueSeedGroup:
        return AttackTechniqueSeedGroup(
            seeds=[
                SeedPrompt(value="tech_a", data_type="text", is_general_technique=True),
                SeedPrompt(value="tech_b", data_type="text", is_general_technique=True),
            ],
            insertion_index=insertion_index,
        )

    def test_append_when_insertion_index_none(self):
        """Test that technique seeds are appended when insertion_index is None."""
        base = self._make_base_group()
        technique = self._make_technique(insertion_index=None)

        merged = base.with_technique(technique=technique)

        assert len(merged.seeds) == 4
        assert merged.seeds[0].value == "objective"
        assert merged.seeds[1].value == "prompt1"
        assert merged.seeds[2].value == "tech_a"
        assert merged.seeds[3].value == "tech_b"

    def test_insert_at_position(self):
        """Test that technique seeds are inserted at the specified position."""
        base = self._make_base_group()
        technique = self._make_technique(insertion_index=1)

        merged = base.with_technique(technique=technique)

        assert len(merged.seeds) == 4
        assert merged.seeds[0].value == "objective"
        assert merged.seeds[1].value == "tech_a"
        assert merged.seeds[2].value == "tech_b"
        assert merged.seeds[3].value == "prompt1"

    def test_insert_at_zero(self):
        """Test insertion_index=0: technique seeds appear right after the objective
        because AttackSeedGroup always places the objective first."""
        base = self._make_base_group()
        technique = self._make_technique(insertion_index=0)

        merged = base.with_technique(technique=technique)

        assert len(merged.seeds) == 4
        # Objective is re-sorted to front by the constructor's canonical ordering
        assert merged.seeds[0].value == "objective"
        assert merged.seeds[1].value == "tech_a"
        assert merged.seeds[2].value == "tech_b"
        assert merged.seeds[3].value == "prompt1"

    def test_insert_beyond_length_appends(self):
        """Test that an insertion_index beyond the list length effectively appends."""
        base = self._make_base_group()
        technique = self._make_technique(insertion_index=999)

        merged = base.with_technique(technique=technique)

        assert len(merged.seeds) == 4
        assert merged.seeds[2].value == "tech_a"
        assert merged.seeds[3].value == "tech_b"

    def test_does_not_mutate_original(self):
        """Test that with_technique returns a new group without mutating the original."""
        base = self._make_base_group()
        technique = self._make_technique()

        merged = base.with_technique(technique=technique)

        assert len(base.seeds) == 2
        assert len(merged.seeds) == 4
        assert merged is not base

    def test_merged_group_is_valid_attack_seed_group(self):
        """Test that the returned group passes AttackSeedGroup validation."""
        base = self._make_base_group()
        technique = self._make_technique()

        merged = base.with_technique(technique=technique)

        assert isinstance(merged, AttackSeedGroup)
        merged._check_invariants()  # should not raise

    def test_system_prompt_technique_merges_onto_user_turn_at_sequence_zero(self):
        """A ``from_system_prompt`` technique merges onto a group whose first turn is a
        ``user`` prompt at sequence 0 without a same-sequence role collision.

        Reproduces the adaptive-scenario failure: merging the ``flip`` technique (a
        ``from_system_prompt`` system seed) onto a multi-turn objective group whose opening
        turn is a ``user`` prompt at sequence 0 raised ``Inconsistent roles found for
        sequence 0``. The leading system seed is normalized to sequence 0 and the existing
        turns shift up (user 0 -> 1, assistant 1 -> 2, ...).
        """
        base = AttackSeedGroup(
            seeds=[
                SeedObjective(value="objective"),
                SeedPrompt(value="opening user turn", data_type="text", role="user", sequence=0),
                SeedPrompt(value="assistant reply", data_type="text", role="assistant", sequence=1),
                SeedPrompt(value="follow-up user turn", data_type="text", role="user", sequence=2),
            ]
        )
        technique = AttackTechniqueSeedGroup.from_system_prompt("Follow these rules.")

        merged = base.with_technique(technique=technique)

        merged._check_invariants()  # should not raise
        system_prompts = [p for p in merged.prompts if p.role == "system"]
        assert len(system_prompts) == 1
        # The leading system seed is normalized to sequence 0 and the base turns shift up.
        assert system_prompts[0].sequence == 0
        assert merged.prompts[0].role == "system"
        assert [(p.role, p.sequence) for p in merged.prompts] == [
            ("system", 0),
            ("user", 1),
            ("assistant", 2),
            ("user", 3),
        ]

    def test_system_prompt_technique_prepends_when_base_uses_negative_sequence(self):
        """Explicit prepend placement must not reserve a sequence value in the base group."""
        base = AttackSeedGroup(
            seeds=[
                SeedObjective(value="objective"),
                SeedPrompt(value="opening user turn", data_type="text", role="user", sequence=-1),
                SeedPrompt(value="assistant reply", data_type="text", role="assistant", sequence=4),
            ]
        )
        technique = AttackTechniqueSeedGroup.from_system_prompt("Follow these rules.")

        merged = base.with_technique(technique=technique)

        assert [(p.role, p.sequence) for p in merged.prompts] == [
            ("system", 0),
            ("user", 1),
            ("assistant", 2),
        ]

    def test_raises_when_technique_has_simulated_conversation_and_prompts_overlap(self):
        """Merging a technique with SeedSimulatedConversation into a group with overlapping prompts raises."""
        base = AttackSeedGroup(
            seeds=[
                SeedObjective(value="objective"),
                SeedPrompt(value="turn_user", data_type="text", role="user", sequence=0),
                SeedPrompt(value="turn_assistant", data_type="text", role="assistant", sequence=1),
                SeedPrompt(value="turn_user_2", data_type="text", role="user", sequence=2),
            ]
        )
        technique = AttackTechniqueSeedGroup(
            seeds=[
                SeedSimulatedConversation(
                    adversarial_chat_system_prompt_path="fake_path.yaml",
                    num_turns=3,
                ),
            ],
        )

        with pytest.raises(ValueError, match="Cannot merge technique containing a SeedSimulatedConversation"):
            base.with_technique(technique=technique)

    def test_succeeds_when_technique_has_simulated_conversation_and_no_prompts(self):
        """Merging a technique with SeedSimulatedConversation into an objective-only group works."""
        base = AttackSeedGroup(seeds=[SeedObjective(value="objective")])
        technique = AttackTechniqueSeedGroup(
            seeds=[
                SeedSimulatedConversation(
                    adversarial_chat_system_prompt_path="fake_path.yaml",
                    num_turns=3,
                ),
            ],
        )

        merged = base.with_technique(technique=technique)
        assert isinstance(merged, AttackSeedGroup)

    def test_is_compatible_returns_false_when_prompts_overlap_simulated_range(self):
        """is_compatible_with_technique returns False when prompt sequences overlap simulated range."""
        base = AttackSeedGroup(
            seeds=[
                SeedObjective(value="objective"),
                SeedPrompt(value="turn_user", data_type="text", role="user", sequence=0),
                SeedPrompt(value="turn_assistant", data_type="text", role="assistant", sequence=1),
                SeedPrompt(value="turn_user_2", data_type="text", role="user", sequence=2),
            ]
        )
        technique = AttackTechniqueSeedGroup(
            seeds=[
                SeedSimulatedConversation(
                    adversarial_chat_system_prompt_path="fake_path.yaml",
                    num_turns=3,
                ),
            ],
        )

        assert not base.is_compatible_with_technique(technique=technique)

    def test_is_compatible_returns_true_for_objective_only_with_simulated(self):
        """is_compatible_with_technique returns True for objective-only base + simulated technique."""
        base = AttackSeedGroup(seeds=[SeedObjective(value="objective")])
        technique = AttackTechniqueSeedGroup(
            seeds=[
                SeedSimulatedConversation(
                    adversarial_chat_system_prompt_path="fake_path.yaml",
                    num_turns=3,
                ),
            ],
        )

        assert base.is_compatible_with_technique(technique=technique)

    def test_is_compatible_returns_true_when_no_simulated_conversation(self):
        """is_compatible_with_technique returns True when technique has no simulated conversation."""
        base = AttackSeedGroup(
            seeds=[
                SeedObjective(value="objective"),
                SeedPrompt(value="turn_user", data_type="text", role="user", sequence=0),
                SeedPrompt(value="turn_assistant", data_type="text", role="assistant", sequence=1),
                SeedPrompt(value="turn_user_2", data_type="text", role="user", sequence=2),
            ]
        )
        technique = self._make_technique()

        assert base.is_compatible_with_technique(technique=technique)


# =============================================================================
# AttackSeedGroup.filter_compatible Tests
# =============================================================================


class TestAttackSeedGroupFilterCompatible:
    """Tests for AttackSeedGroup.filter_compatible() static method."""

    def test_filters_out_incompatible_groups(self):
        """filter_compatible removes groups whose prompts overlap with simulated conversation."""
        compatible = AttackSeedGroup(
            seeds=[SeedObjective(value="obj1")],
        )
        incompatible = AttackSeedGroup(
            seeds=[
                SeedObjective(value="obj2"),
                SeedPrompt(value="u", data_type="text", role="user", sequence=0),
                SeedPrompt(value="a", data_type="text", role="assistant", sequence=1),
                SeedPrompt(value="u2", data_type="text", role="user", sequence=2),
            ],
        )
        technique = AttackTechniqueSeedGroup(
            seeds=[
                SeedSimulatedConversation(
                    adversarial_chat_system_prompt_path="fake.yaml",
                    num_turns=3,
                ),
            ],
        )

        result = AttackSeedGroup.filter_compatible(
            seed_groups=[compatible, incompatible],
            technique=technique,
        )

        assert len(result) == 1
        assert result[0].objective.value == "obj1"

    def test_returns_all_when_no_simulated_conversation(self):
        """filter_compatible returns all groups when technique has no simulated conversation."""
        groups = [
            AttackSeedGroup(
                seeds=[
                    SeedObjective(value="obj"),
                    SeedPrompt(value="u", data_type="text", role="user", sequence=0),
                    SeedPrompt(value="a", data_type="text", role="assistant", sequence=1),
                    SeedPrompt(value="u2", data_type="text", role="user", sequence=2),
                ],
            ),
        ]
        technique = AttackTechniqueSeedGroup(
            seeds=[SeedPrompt(value="tech", data_type="text", is_general_technique=True)],
        )

        result = AttackSeedGroup.filter_compatible(seed_groups=groups, technique=technique)
        assert len(result) == 1
