# tests/test_wave1_perturbation.py
"""Tests for MinPerturbationAttacker (wave1 29.6)."""
from __future__ import annotations

import sys
from pathlib import Path
import pytest

ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))


SAMPLE_STEPS = [
    {"thought": "I need to find information about this topic.",
     "action_type": "Search", "action_arg": "relevant query",
     "observation": "Wikipedia: The answer is clearly documented."},
    {"thought": "Based on the search I found the answer.",
     "action_type": "Finish", "action_arg": "The answer is correct.",
     "observation": ""},
]


def test_import():
    from llm_guard.adversarial.min_perturbation_attacker import MinPerturbationAttacker
    assert MinPerturbationAttacker is not None


def test_instantiates():
    from llm_guard.adversarial.min_perturbation_attacker import MinPerturbationAttacker
    att = MinPerturbationAttacker()
    assert att.budget == 10


def test_custom_budget():
    from llm_guard.adversarial.min_perturbation_attacker import MinPerturbationAttacker
    att = MinPerturbationAttacker(budget=5)
    assert att.budget == 5


def test_get_synonyms_returns_list():
    from llm_guard.adversarial.min_perturbation_attacker import MinPerturbationAttacker
    att = MinPerturbationAttacker()
    syns = att.get_synonyms("information")
    assert isinstance(syns, list)
    # May be empty if nltk not downloaded, but must return list
    assert all(isinstance(s, str) for s in syns)


def test_attack_returns_perturbed_steps_and_count():
    """attack() without a real detector just runs structural perturbation."""
    from llm_guard.adversarial.min_perturbation_attacker import MinPerturbationAttacker

    class MockDetector:
        """Always predicts adversarial=True (score=1.0)."""
        def predict_proba(self, chain):
            return 1.0  # always adversarial

    att = MinPerturbationAttacker(budget=3)
    result = att.attack(SAMPLE_STEPS, "What is the answer?",
                        MockDetector(), budget=3)
    assert "perturbed_steps" in result
    assert "n_substitutions" in result
    assert isinstance(result["n_substitutions"], int)
    assert result["n_substitutions"] >= 0


def test_attack_respects_budget():
    from llm_guard.adversarial.min_perturbation_attacker import MinPerturbationAttacker

    class MockDetector:
        def predict_proba(self, chain):
            return 0.9

    att = MinPerturbationAttacker(budget=2)
    result = att.attack(SAMPLE_STEPS, "question", MockDetector(), budget=2)
    assert result["n_substitutions"] <= 2


def test_perturbed_steps_have_same_structure():
    from llm_guard.adversarial.min_perturbation_attacker import MinPerturbationAttacker

    class MockDetector:
        def predict_proba(self, chain):
            return 0.9

    att = MinPerturbationAttacker(budget=3)
    result = att.attack(SAMPLE_STEPS, "question", MockDetector())
    for step in result["perturbed_steps"]:
        assert "thought" in step
        assert "action_type" in step
        assert "action_arg" in step
        assert "observation" in step
