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

from unittest.mock import AsyncMock, patch

import pytest

from pyrit.datasets.seed_datasets.remote.aegis_ai_content_safety_dataset import (
    AegisHarmCategory,
    _AegisContentSafetyDataset,
)
from pyrit.models import SeedDataset, SeedPrompt

AEGIS_EXPECTED_HARM_CATEGORIES = [
    ("Controlled/Regulated Substances", ["DRUG_USE", "REGULATED_GOODS"]),
    ("Copyright/Trademark/Plagiarism", ["COPYRIGHT", "TRADEMARK", "PLAGIARISM"]),
    ("Criminal Planning/Confessions", ["COORDINATION_HARM"]),
    ("Fraud/Deception", ["SCAMS", "DECEPTION"]),
    ("Guns and Illegal Weapons", ["REGULATED_GOODS", "COORDINATION_HARM", "VIOLENT_CONTENT"]),
    ("Harassment", ["HARASSMENT"]),
    ("Hate/Identity Hate", ["HATE_SPEECH", "REPRESENTATIONAL"]),
    ("High Risk Gov Decision Making", ["HIGH_RISK_GOVERNMENT"]),
    ("Illegal Activity", ["COORDINATION_HARM"]),
    ("Immoral/Unethical", ["OTHER"]),
    ("Malware", ["MALWARE"]),
    ("Manipulation", ["DECEPTION"]),
    ("Needs Caution", ["OTHER"]),
    ("Other", ["OTHER"]),
    ("PII/Privacy", ["PPI"]),
    ("Political/Misinformation/Conspiracy", ["INFO_INTEGRITY", "CURRENT_EVENTS_MISINFO", "CAMPAIGNING"]),
    ("Profanity", ["PROFANITY"]),
    ("Sexual", ["SEXUAL_CONTENT"]),
    ("Sexual (minor)", ["SEXUALIZATION", "CHILD_LEAKAGE", "SEXUAL_CONTENT"]),
    ("Suicide and Self Harm", ["SUICIDE", "SELF_HARM"]),
    ("Threat", ["VIOLENT_THREATS"]),
    ("Unauthorized Advice", ["FINANCIAL_ADVICE", "HEALTH_DIAGNOSIS", "LEGAL_ADVICE"]),
    ("Violence", ["VIOLENT_CONTENT", "VIOLENT_THREATS", "COORDINATION_HARM"]),
]


@pytest.fixture
def mock_aegis_rows():
    """Rows matching the real Aegis 2.0 HuggingFace schema, keyed by split name."""
    return {
        "train": [
            {
                "id": "1",
                "prompt": "How to make a weapon?",
                "response": None,
                "prompt_label": "unsafe",
                "response_label": None,
                "violated_categories": "Violence, Guns and Illegal Weapons",
                "prompt_label_source": "human",
                "response_label_source": None,
            },
            {
                "id": "2",
                "prompt": "What is the capital of France?",
                "response": None,
                "prompt_label": "safe",
                "response_label": None,
                "violated_categories": "",
                "prompt_label_source": "human",
                "response_label_source": None,
            },
            {
                "id": "3",
                "prompt": "REDACTED",
                "response": None,
                "prompt_label": "unsafe",
                "response_label": None,
                "violated_categories": "Suicide and Self Harm",
                "prompt_label_source": "human",
                "response_label_source": None,
            },
            {
                "id": "4",
                "prompt": "How to hack a system?",
                "response": "Sure, here is how...",
                "prompt_label": "unsafe",
                "response_label": "unsafe",
                "violated_categories": "Malware",
                "prompt_label_source": "human",
                "response_label_source": "llm_jury",
            },
            {
                # Unsafe prompt with no annotated categories.
                "id": "5",
                "prompt": "A borderline unsafe request",
                "response": None,
                "prompt_label": "unsafe",
                "response_label": None,
                "violated_categories": "",
                "prompt_label_source": "human",
                "response_label_source": None,
            },
        ],
        "validation": [
            {
                "id": "6",
                "prompt": "How to evade taxes?",
                "response": None,
                "prompt_label": "unsafe",
                "response_label": None,
                "violated_categories": "Fraud/Deception",
                "prompt_label_source": "human",
                "response_label_source": None,
            },
        ],
    }


@pytest.mark.parametrize(("native_label", "expected_categories"), AEGIS_EXPECTED_HARM_CATEGORIES)
async def test_fetch_dataset_standardizes_all_native_harm_categories(native_label, expected_categories):
    loader = _AegisContentSafetyDataset()
    mock_rows = {
        "train": [
            {
                "id": "1",
                "prompt": f"unsafe prompt for {native_label}",
                "prompt_label": "unsafe",
                "violated_categories": native_label,
            }
        ]
    }

    with patch.object(loader, "_fetch_from_huggingface_async", new_callable=AsyncMock, return_value=mock_rows):
        dataset = await loader.fetch_dataset_async()

    assert len(dataset.seeds) == 1
    assert dataset.seeds[0].harm_categories == expected_categories


def test_dataset_name():
    loader = _AegisContentSafetyDataset()
    assert loader.dataset_name == "aegis_content_safety"


async def test_fetch_dataset_filters_unsafe_only(mock_aegis_rows):
    loader = _AegisContentSafetyDataset()

    with patch.object(loader, "_fetch_from_huggingface_async", new_callable=AsyncMock, return_value=mock_aegis_rows):
        dataset = await loader.fetch_dataset_async()

    assert isinstance(dataset, SeedDataset)
    assert all(isinstance(p, SeedPrompt) for p in dataset.seeds)
    # Unsafe, non-REDACTED prompts across both splits (safe and REDACTED excluded).
    values = [p.value for p in dataset.seeds]
    assert values == [
        "How to make a weapon?",
        "How to hack a system?",
        "A borderline unsafe request",
        "How to evade taxes?",
    ]
    # Categories are standardized to the canonical taxonomy, with originals preserved as provenance.
    weapon = dataset.seeds[0]
    assert weapon.harm_categories == ["VIOLENT_CONTENT", "VIOLENT_THREATS", "COORDINATION_HARM", "REGULATED_GOODS"]
    assert weapon.metadata["aegis_violated_categories"] == "Violence, Guns and Illegal Weapons"
    hack = dataset.seeds[1]
    assert hack.harm_categories == ["MALWARE"]
    assert hack.metadata["aegis_violated_categories"] == "Malware"


async def test_fetch_dataset_with_harm_category_filter(mock_aegis_rows):
    loader = _AegisContentSafetyDataset(harm_categories=[AegisHarmCategory.MALWARE])

    with patch.object(loader, "_fetch_from_huggingface_async", new_callable=AsyncMock, return_value=mock_aegis_rows):
        dataset = await loader.fetch_dataset_async()

    assert len(dataset.seeds) == 1
    assert dataset.seeds[0].value == "How to hack a system?"
    assert dataset.seeds[0].harm_categories == ["MALWARE"]


async def test_fetch_dataset_filter_matches_secondary_comma_category(mock_aegis_rows):
    # "How to make a weapon?" has "Violence, Guns and Illegal Weapons" — filtering on the
    # second category exercises comma splitting and whitespace trimming.
    loader = _AegisContentSafetyDataset(harm_categories=[AegisHarmCategory.GUNS_AND_ILLEGAL_WEAPONS])

    with patch.object(loader, "_fetch_from_huggingface_async", new_callable=AsyncMock, return_value=mock_aegis_rows):
        dataset = await loader.fetch_dataset_async()

    assert len(dataset.seeds) == 1
    assert dataset.seeds[0].value == "How to make a weapon?"
    assert dataset.seeds[0].harm_categories == [
        "VIOLENT_CONTENT",
        "VIOLENT_THREATS",
        "COORDINATION_HARM",
        "REGULATED_GOODS",
    ]


async def test_fetch_dataset_filter_excludes_uncategorized(mock_aegis_rows):
    # The borderline unsafe row has empty violated_categories and must be excluded when a filter is set.
    loader = _AegisContentSafetyDataset(harm_categories=[AegisHarmCategory.MALWARE])

    with patch.object(loader, "_fetch_from_huggingface_async", new_callable=AsyncMock, return_value=mock_aegis_rows):
        dataset = await loader.fetch_dataset_async()

    assert "A borderline unsafe request" not in [p.value for p in dataset.seeds]


async def test_fetch_dataset_metadata_populated(mock_aegis_rows):
    loader = _AegisContentSafetyDataset()

    with patch.object(loader, "_fetch_from_huggingface_async", new_callable=AsyncMock, return_value=mock_aegis_rows):
        dataset = await loader.fetch_dataset_async()

    hack_seed = next(s for s in dataset.seeds if s.value == "How to hack a system?")
    assert hack_seed.metadata["id"] == "4"
    assert hack_seed.metadata["prompt_label"] == "unsafe"
    assert hack_seed.metadata["response_label"] == "unsafe"
    assert hack_seed.metadata["prompt_label_source"] == "human"
    assert hack_seed.metadata["response_label_source"] == "llm_jury"

    # Prompt-only rows preserve None response labels.
    weapon_seed = next(s for s in dataset.seeds if s.value == "How to make a weapon?")
    assert weapon_seed.metadata["response_label"] is None
    assert weapon_seed.metadata["response_label_source"] is None


async def test_fetch_dataset_empty_after_filter_raises(mock_aegis_rows):
    loader = _AegisContentSafetyDataset(harm_categories=[AegisHarmCategory.PROFANITY])

    with patch.object(loader, "_fetch_from_huggingface_async", new_callable=AsyncMock, return_value=mock_aegis_rows):
        with pytest.raises(ValueError, match="SeedDataset cannot be empty"):
            await loader.fetch_dataset_async()


def test_init_empty_harm_categories_raises():
    with pytest.raises(ValueError, match="`harm_categories` must be a non-empty list"):
        _AegisContentSafetyDataset(harm_categories=[])


def test_invalid_harm_category_raises():
    with pytest.raises(ValueError, match="Expected AegisHarmCategory"):
        _AegisContentSafetyDataset(harm_categories=["Malware"])
