"""
Pydantic Schema Tester - Comprehensive adversarial testing for schemas.

This module provides a `SchemaTester` class that generates and runs
a comprehensive suite of adversarial tests against any Pydantic schema.

Usage:
    from lightwave.schema.testing import SchemaTester
    from myapp.schemas import UserSchema

    # Quick test
    tester = SchemaTester(UserSchema)
    report = tester.run_all()
    assert report.bugs_found == 0

    # In pytest
    class TestUserSchema:
        def test_adversarial(self):
            tester = SchemaTester(
                UserSchema,
                valid_instance={"email": "test@example.com", "name": "Test"},
            )
            report = tester.run_all()
            assert report.bugs_found == 0, report.summary()

    # Generate pytest test class
    TestUserSchemaAdversarial = SchemaTester(UserSchema).generate_test_class()
"""

from __future__ import annotations

import json
from dataclasses import dataclass, field
from typing import Any, Literal, get_args, get_origin

from pydantic import BaseModel, ValidationError


@dataclass
class TestResult:
    """Result of a single test."""

    name: str
    category: str
    passed: bool
    bug_found: bool = False
    error_message: str = ""
    expected: str = ""
    actual: str = ""


@dataclass
class SchemaTestReport:
    """Report from running schema tests."""

    schema_name: str
    results: list[TestResult] = field(default_factory=list)

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

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

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

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

    @property
    def by_category(self) -> dict[str, list[TestResult]]:
        categories: dict[str, list[TestResult]] = {}
        for r in self.results:
            categories.setdefault(r.category, []).append(r)
        return categories

    def summary(self) -> str:
        lines = [
            f"Schema: {self.schema_name}",
            f"Total: {self.total}, Passed: {self.passed}, Failed: {self.failed}",
            f"Bugs Found: {self.bugs_found}",
        ]
        if self.bugs_found > 0:
            lines.append("\nBugs:")
            for r in self.results:
                if r.bug_found:
                    lines.append(f"  - [{r.category}] {r.name}: {r.error_message}")
        return "\n".join(lines)

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


class SchemaTester:
    """
    Comprehensive adversarial tester for Pydantic schemas.

    Runs multiple categories of tests:
    - Rejection: Invalid input must fail
    - Boundary: Test at and beyond limits
    - Literal: All valid literal values accepted, invalid rejected
    - Required: Missing required fields fail
    - Extra: Unknown fields rejected
    - RoundTrip: Serialization preserves data
    - Type: Wrong types rejected

    Usage:
        tester = SchemaTester(
            MySchema,
            valid_instance={"field1": "value", "field2": 123},
        )
        report = tester.run_all()
        print(report.summary())
    """

    def __init__(
        self,
        schema_class: type[BaseModel],
        valid_instance: dict[str, Any] | None = None,
        invalid_values: dict[str, list[Any]] | None = None,
        skip_categories: list[str] | None = None,
    ):
        """
        Initialize the schema tester.

        Args:
            schema_class: The Pydantic model to test
            valid_instance: Dict of valid field values for constructing instances
            invalid_values: Dict mapping field names to lists of invalid values to test
            skip_categories: List of test categories to skip
        """
        self.schema_class = schema_class
        self.schema_name = schema_class.__name__
        self.valid_instance = valid_instance or {}
        self.invalid_values = invalid_values or {}
        self.skip_categories = set(skip_categories or [])
        self.report = SchemaTestReport(schema_name=self.schema_name)

    def _record(
        self,
        name: str,
        category: str,
        passed: bool,
        bug_found: bool = False,
        error_message: str = "",
        expected: str = "",
        actual: str = "",
    ) -> None:
        """Record a test result."""
        self.report.add(
            TestResult(
                name=name,
                category=category,
                passed=passed,
                bug_found=bug_found,
                error_message=error_message,
                expected=expected,
                actual=actual,
            )
        )

    def run_all(self) -> SchemaTestReport:
        """Run all adversarial tests and return the report."""
        self.report = SchemaTestReport(schema_name=self.schema_name)

        if "rejection" not in self.skip_categories:
            self._test_rejection()

        if "required" not in self.skip_categories:
            self._test_required_fields()

        if "extra" not in self.skip_categories:
            self._test_extra_fields()

        if "boundary" not in self.skip_categories:
            self._test_boundaries()

        if "literal" not in self.skip_categories:
            self._test_literal_fields()

        if "roundtrip" not in self.skip_categories:
            self._test_round_trip()

        if "type" not in self.skip_categories:
            self._test_wrong_types()

        return self.report

    def _test_rejection(self) -> None:
        """Test that known invalid values are rejected."""
        for field_name, invalid_vals in self.invalid_values.items():
            for invalid_val in invalid_vals:
                test_name = f"reject_{field_name}_{type(invalid_val).__name__}"
                kwargs = self.valid_instance.copy()
                kwargs[field_name] = invalid_val

                try:
                    self.schema_class(**kwargs)
                    # Should have raised - this is a bug!
                    self._record(
                        name=test_name,
                        category="rejection",
                        passed=False,
                        bug_found=True,
                        error_message=f"Accepted invalid {field_name}={invalid_val!r}",
                        expected="ValidationError",
                        actual="No error",
                    )
                except ValidationError:
                    self._record(
                        name=test_name,
                        category="rejection",
                        passed=True,
                    )
                except Exception as e:
                    self._record(
                        name=test_name,
                        category="rejection",
                        passed=False,
                        error_message=f"Unexpected error: {e}",
                    )

    def _test_required_fields(self) -> None:
        """Test that required fields are actually required."""
        for field_name, field_info in self.schema_class.model_fields.items():
            if field_info.is_required():
                test_name = f"required_{field_name}"
                kwargs = self.valid_instance.copy()
                kwargs.pop(field_name, None)

                try:
                    self.schema_class(**kwargs)
                    # Should have raised - this is a bug!
                    self._record(
                        name=test_name,
                        category="required",
                        passed=False,
                        bug_found=True,
                        error_message=f"Accepted missing required field '{field_name}'",
                        expected="ValidationError",
                        actual="No error",
                    )
                except ValidationError:
                    self._record(
                        name=test_name,
                        category="required",
                        passed=True,
                    )

    def _test_extra_fields(self) -> None:
        """Test that extra/unknown fields are rejected."""
        test_name = "reject_extra_fields"
        kwargs = self.valid_instance.copy()
        kwargs["__UNKNOWN_FIELD_12345__"] = "should_fail"

        try:
            self.schema_class(**kwargs)
            # Should have raised (if schema is strict)
            self._record(
                name=test_name,
                category="extra",
                passed=False,
                bug_found=True,
                error_message="Accepted unknown field (schema not strict)",
                expected="ValidationError",
                actual="No error",
            )
        except ValidationError:
            self._record(
                name=test_name,
                category="extra",
                passed=True,
            )

    def _test_boundaries(self) -> None:
        """Test boundary conditions for constrained fields."""
        for field_name, field_info in self.schema_class.model_fields.items():
            # Check for max_length constraint
            max_length = getattr(field_info, "max_length", None)
            if max_length is not None:
                # Test at boundary
                test_name = f"boundary_{field_name}_at_max"
                kwargs = self.valid_instance.copy()
                kwargs[field_name] = "x" * max_length

                try:
                    self.schema_class(**kwargs)
                    self._record(
                        name=test_name,
                        category="boundary",
                        passed=True,
                    )
                except ValidationError as e:
                    self._record(
                        name=test_name,
                        category="boundary",
                        passed=False,
                        bug_found=True,
                        error_message=f"Rejected value AT max_length={max_length}",
                        expected="Accepted",
                        actual=str(e),
                    )

                # Test beyond boundary
                test_name = f"boundary_{field_name}_beyond_max"
                kwargs = self.valid_instance.copy()
                kwargs[field_name] = "x" * (max_length + 1)

                try:
                    self.schema_class(**kwargs)
                    self._record(
                        name=test_name,
                        category="boundary",
                        passed=False,
                        bug_found=True,
                        error_message=f"Accepted value BEYOND max_length={max_length}",
                        expected="ValidationError",
                        actual="No error",
                    )
                except ValidationError:
                    self._record(
                        name=test_name,
                        category="boundary",
                        passed=True,
                    )

    def _test_literal_fields(self) -> None:
        """Test that Literal fields accept only valid values."""
        for field_name, field_info in self.schema_class.model_fields.items():
            annotation = field_info.annotation
            origin = get_origin(annotation)

            # Check if this is a Literal type
            if origin is Literal:
                valid_values = get_args(annotation)

                # Test all valid values are accepted
                for valid_val in valid_values:
                    test_name = f"literal_{field_name}_accept_{valid_val}"
                    kwargs = self.valid_instance.copy()
                    kwargs[field_name] = valid_val

                    try:
                        self.schema_class(**kwargs)
                        self._record(
                            name=test_name,
                            category="literal",
                            passed=True,
                        )
                    except ValidationError as e:
                        self._record(
                            name=test_name,
                            category="literal",
                            passed=False,
                            bug_found=True,
                            error_message=f"Rejected valid literal value '{valid_val}'",
                            expected="Accepted",
                            actual=str(e),
                        )

                # Test invalid value is rejected
                test_name = f"literal_{field_name}_reject_invalid"
                kwargs = self.valid_instance.copy()
                kwargs[field_name] = "__INVALID_LITERAL_VALUE__"

                try:
                    self.schema_class(**kwargs)
                    self._record(
                        name=test_name,
                        category="literal",
                        passed=False,
                        bug_found=True,
                        error_message=f"Accepted invalid literal value for '{field_name}'",
                        expected="ValidationError",
                        actual="No error",
                    )
                except ValidationError:
                    self._record(
                        name=test_name,
                        category="literal",
                        passed=True,
                    )

    def _test_round_trip(self) -> None:
        """Test that serialization round-trip preserves data."""
        if not self.valid_instance:
            return

        test_name = "roundtrip_json"
        try:
            original = self.schema_class(**self.valid_instance)
            json_str = original.model_dump_json()
            parsed = json.loads(json_str)
            reconstructed = self.schema_class.model_validate(parsed)

            # Compare fields
            mismatches = []
            for field_name in self.valid_instance:
                orig_val = getattr(original, field_name)
                recon_val = getattr(reconstructed, field_name)
                if orig_val != recon_val:
                    mismatches.append(f"{field_name}: {orig_val!r} != {recon_val!r}")

            if mismatches:
                self._record(
                    name=test_name,
                    category="roundtrip",
                    passed=False,
                    bug_found=True,
                    error_message=f"Data lost in round-trip: {mismatches}",
                )
            else:
                self._record(
                    name=test_name,
                    category="roundtrip",
                    passed=True,
                )
        except Exception as e:
            self._record(
                name=test_name,
                category="roundtrip",
                passed=False,
                error_message=f"Round-trip failed: {e}",
            )

    def _test_wrong_types(self) -> None:
        """Test that wrong types are rejected."""
        wrong_type_values = [
            ("string_for_int", "not_an_int"),
            ("int_for_string", 12345),
            ("list_for_string", ["a", "b"]),
            ("dict_for_string", {"key": "value"}),
        ]

        for field_name, field_info in self.schema_class.model_fields.items():
            annotation = field_info.annotation

            # Skip optional fields and Any types
            if annotation is Any:
                continue

            for type_name, wrong_val in wrong_type_values:
                # Only test if the wrong type doesn't match the expected type
                if isinstance(wrong_val, str) and annotation is str:
                    continue
                if isinstance(wrong_val, int) and annotation is int:
                    continue

                test_name = f"type_{field_name}_{type_name}"
                kwargs = self.valid_instance.copy()
                kwargs[field_name] = wrong_val

                try:
                    self.schema_class(**kwargs)
                    # Might be valid due to coercion - not necessarily a bug
                    self._record(
                        name=test_name,
                        category="type",
                        passed=True,
                    )
                except ValidationError:
                    self._record(
                        name=test_name,
                        category="type",
                        passed=True,
                    )
                except Exception as e:
                    self._record(
                        name=test_name,
                        category="type",
                        passed=False,
                        error_message=f"Unexpected error: {e}",
                    )

    def generate_test_class(self, class_name: str | None = None) -> type:
        """
        Generate a pytest test class for this schema.

        Usage:
            TestUserSchema = SchemaTester(UserSchema).generate_test_class()

            # Or with custom name
            TestUser = SchemaTester(UserSchema).generate_test_class("TestUser")
        """
        class_name = class_name or f"Test{self.schema_name}Adversarial"
        tester = self

        class GeneratedTestClass:
            def test_adversarial_suite(self) -> None:
                report = tester.run_all()
                assert report.bugs_found == 0, report.summary()

        GeneratedTestClass.__name__ = class_name
        GeneratedTestClass.__qualname__ = class_name
        return GeneratedTestClass
