"""Tests for SAFE-MCP parameterized scenario generator."""

from __future__ import annotations

import os
import json
import pytest
from unittest.mock import patch

from navil.safemcp.generator import (
    AttackVariantGenerator,
    generate_all_variants,
    load_attack_catalog,
    _CATEGORY_TO_GENERATOR,
)


# ── Catalog Loading ──────────────────────────────────────────


class TestLoadAttackCatalog:
    """Tests for loading the YAML attack catalog."""

    def test_load_default_catalog(self):
        """Should load the bundled public_attacks.yaml."""
        attacks = load_attack_catalog()
        assert len(attacks) >= 30, f"Expected 30+ attacks, got {len(attacks)}"

    def test_each_attack_has_required_fields(self):
        """Every attack must have name, category, severity, attack_steps."""
        attacks = load_attack_catalog()
        for attack in attacks:
            assert "name" in attack, f"Attack missing 'name': {attack}"
            assert "category" in attack, f"Attack {attack['name']} missing 'category'"
            assert "severity" in attack, f"Attack {attack['name']} missing 'severity'"
            assert "attack_steps" in attack, f"Attack {attack['name']} missing 'attack_steps'"
            assert "description" in attack, f"Attack {attack['name']} missing 'description'"

    def test_categories_map_to_generators(self):
        """Every attack category should have a corresponding variant generator."""
        attacks = load_attack_catalog()
        categories = {a["category"] for a in attacks}
        for cat in categories:
            assert cat in _CATEGORY_TO_GENERATOR, f"No generator for category: {cat}"

    def test_severity_values_are_valid(self):
        """Severity must be one of LOW, MEDIUM, HIGH, CRITICAL."""
        valid = {"LOW", "MEDIUM", "HIGH", "CRITICAL"}
        attacks = load_attack_catalog()
        for attack in attacks:
            assert attack["severity"] in valid, (
                f"Invalid severity '{attack['severity']}' in attack '{attack['name']}'"
            )

    def test_no_duplicate_names(self):
        """Attack names must be unique."""
        attacks = load_attack_catalog()
        names = [a["name"] for a in attacks]
        assert len(names) == len(set(names)), f"Duplicate attack names found"


# ── Variant Generation ───────────────────────────────────────


class TestAttackVariantGenerator:
    """Tests for the parameterized variant generator."""

    @pytest.fixture
    def generator(self):
        gen = AttackVariantGenerator(variants_per_attack=5)
        gen.load()
        return gen

    def test_load_succeeds(self, generator):
        assert len(generator.attacks) >= 30

    def test_attack_names_populated(self, generator):
        names = generator.attack_names
        assert len(names) >= 30
        assert "github_data_heist_via_copilot" in names

    def test_generate_variants_all(self, generator):
        """Should generate variants for all attacks."""
        variants = generator.generate_variants()
        assert len(variants) >= 25  # some categories might not map

        for attack_name, variant_list in variants.items():
            assert len(variant_list) >= 5, (
                f"Attack '{attack_name}' produced only {len(variant_list)} variants (expected >=5)"
            )
            assert len(variant_list) <= 10, (
                f"Attack '{attack_name}' produced {len(variant_list)} variants (expected <=10)"
            )

    def test_generate_variants_single(self, generator):
        """Should generate variants for a single named attack."""
        variants = generator.generate_variants("github_data_heist_via_copilot")
        assert len(variants) == 1
        assert "github_data_heist_via_copilot" in variants

    def test_variant_invocation_format(self, generator):
        """Each variant invocation must have agent_name and tool_name."""
        variants = generator.generate_variants()
        for attack_name, variant_list in variants.items():
            for variant in variant_list:
                assert isinstance(variant, list), f"Variant for {attack_name} is not a list"
                for inv in variant:
                    assert isinstance(inv, dict), f"Invocation is not a dict"
                    assert "agent_name" in inv, f"Missing agent_name in {attack_name}"
                    assert "tool_name" in inv, f"Missing tool_name in {attack_name}"

    def test_generate_scenario_generators(self, generator):
        """Should produce callable generators compatible with seed.py."""
        generators = generator.generate_scenario_generators()
        assert len(generators) >= 25

        for name, gen_fn in generators.items():
            result = gen_fn("test-agent-42", 0)
            assert isinstance(result, list), f"Generator {name} returned non-list"
            assert len(result) >= 1, f"Generator {name} returned empty list"

            for inv in result:
                assert inv["agent_name"] == "test-agent-42", (
                    f"Generator {name} didn't override agent_name"
                )

    def test_export_scenarios(self, generator):
        """Should produce JSON-serializable scenario definitions."""
        exported = generator.export_scenarios()
        assert len(exported) >= 30

        # Verify JSON-serializable
        json_str = json.dumps(exported)
        parsed = json.loads(json_str)
        assert len(parsed) == len(exported)

        for entry in exported:
            assert "name" in entry
            assert "category" in entry
            assert "severity" in entry


class TestGenerateAllVariants:
    """Tests for the convenience function."""

    def test_generates_variants(self):
        result = generate_all_variants(variants_per_attack=5)
        assert isinstance(result, dict)
        assert len(result) >= 25


# ── Parameter Bounds ─────────────────────────────────────────


class TestParameterBounds:
    """Ensure generated variants respect expected bounds."""

    @pytest.fixture
    def all_variants(self):
        gen = AttackVariantGenerator(variants_per_attack=5)
        gen.load()
        return gen.generate_variants()

    def test_agent_names_are_unique_per_variant(self, all_variants):
        """Different variants should generally have different agent names."""
        for attack_name, variant_list in all_variants.items():
            agents = set()
            for variant in variant_list:
                if variant:
                    agents.add(variant[0]["agent_name"])
            # With random agent names, most should be unique
            assert len(agents) >= 3, (
                f"Attack '{attack_name}' has too few unique agents: {len(agents)}"
            )

    def test_invocations_have_valid_durations(self, all_variants):
        """duration_ms should be positive."""
        for attack_name, variant_list in all_variants.items():
            for variant in variant_list:
                for inv in variant:
                    if "duration_ms" in inv:
                        assert inv["duration_ms"] >= 0, (
                            f"Negative duration in {attack_name}"
                        )
