"""
Root pytest configuration and shared fixtures.

Provides fixture freshness validation and common test utilities.
"""

import json
import warnings
from pathlib import Path
from typing import Any, Dict, Optional, Set, Union

import pytest
from mcp.types import TextContent

# Fixture schema version - increment when fixture format changes
FIXTURE_SCHEMA_VERSION = "1.0.0"

# Response contract version from responses.py
RESPONSE_CONTRACT_VERSION = "response-v2"

# Track validated fixtures to avoid repeated checks
_validated_fixtures: Set[Path] = set()


def extract_response_dict(result: Union[Dict[str, Any], TextContent]) -> Dict[str, Any]:
    """Extract dict from tool result, handling both dict and TextContent.

    Tools wrapped with canonical_tool decorator now return TextContent with
    minified JSON. This helper extracts the dict for test assertions.

    Args:
        result: Tool result - either dict (non-minified) or TextContent (minified)

    Returns:
        Parsed dictionary from the response

    Raises:
        TypeError: If result is neither dict nor TextContent
        json.JSONDecodeError: If TextContent.text is not valid JSON
    """
    if isinstance(result, dict):
        return result
    if isinstance(result, TextContent):
        return json.loads(result.text)
    raise TypeError(
        f"Expected dict or TextContent, got {type(result).__name__}"
    )


def get_fixture_version(fixture_path: Path) -> Optional[str]:
    """Extract version from a JSON fixture file.

    Looks for version in these locations (in order):
    1. Top-level "fixture_version" key
    2. "metadata.version" key
    3. "_fixture_meta.version" key

    Returns None if no version found.
    """
    try:
        with open(fixture_path) as f:
            data = json.load(f)

        # Check top-level fixture_version
        if "fixture_version" in data:
            return data["fixture_version"]

        # Check metadata.version
        if isinstance(data.get("metadata"), dict):
            if "version" in data["metadata"]:
                return data["metadata"]["version"]

        # Check _fixture_meta.version (alternative location)
        if isinstance(data.get("_fixture_meta"), dict):
            if "version" in data["_fixture_meta"]:
                return data["_fixture_meta"]["version"]

        return None
    except (json.JSONDecodeError, IOError):
        return None


def validate_fixture_freshness(
    fixture_path: Path,
    expected_version: str = FIXTURE_SCHEMA_VERSION,
    strict: bool = False,
) -> bool:
    """Validate that a fixture file's version matches expected version.

    Args:
        fixture_path: Path to JSON fixture file
        expected_version: Expected fixture schema version
        strict: If True, raise error on mismatch; if False, warn

    Returns:
        True if version matches or no version found (lenient mode)

    Raises:
        ValueError: In strict mode when version mismatch detected
    """
    if fixture_path in _validated_fixtures:
        return True

    version = get_fixture_version(fixture_path)

    if version is None:
        # No version in fixture - warn but don't fail
        warnings.warn(
            f"Fixture {fixture_path.name} has no version marker. "
            f"Consider adding 'fixture_version': '{FIXTURE_SCHEMA_VERSION}'",
            UserWarning,
            stacklevel=2,
        )
        _validated_fixtures.add(fixture_path)
        return True

    if version != expected_version:
        msg = (
            f"Fixture {fixture_path.name} has version {version}, "
            f"expected {expected_version}. Fixture may be stale."
        )
        if strict:
            raise ValueError(msg)
        else:
            warnings.warn(msg, UserWarning, stacklevel=2)
            _validated_fixtures.add(fixture_path)
            return False

    _validated_fixtures.add(fixture_path)
    return True


def validate_response_envelope(response: Dict[str, Any]) -> bool:
    """Validate that a response dict conforms to response-v2 envelope.

    Args:
        response: Response dictionary to validate

    Returns:
        True if valid, False otherwise

    Raises:
        AssertionError: With detailed message on validation failure
    """
    # Check required top-level keys
    required_keys = {"success", "data", "error", "meta"}
    missing = required_keys - set(response.keys())
    assert not missing, f"Response missing required keys: {missing}"

    # Check types
    assert isinstance(response["success"], bool), "success must be boolean"
    assert isinstance(response["data"], dict), "data must be dict"
    assert isinstance(response["meta"], dict), "meta must be dict"

    # Check success/error consistency
    if response["success"]:
        assert response["error"] is None, "error must be null when success=True"
    else:
        assert isinstance(response["error"], str) and response["error"], (
            "error must be non-empty string when success=False"
        )

    # Check meta.version
    assert response["meta"].get("version") == RESPONSE_CONTRACT_VERSION, (
        f"meta.version must be '{RESPONSE_CONTRACT_VERSION}'"
    )

    return True


@pytest.fixture(scope="session", autouse=True)
def validate_fixture_versions(request):
    """Autouse fixture that validates fixture file versions at session start.

    Scans known fixture directories and warns about stale fixtures.
    This helps catch outdated test data that may cause false positives/negatives.
    """
    # Get test root directory
    test_root = Path(__file__).parent

    # Known fixture directories to scan
    fixture_dirs = [
        test_root / "fixtures",
        test_root / "parity" / "fixtures",
        test_root / "contract",
    ]

    stale_count = 0
    checked_count = 0

    for fixture_dir in fixture_dirs:
        if not fixture_dir.exists():
            continue

        for json_file in fixture_dir.rglob("*.json"):
            # Skip schema files (they define the version, not use it)
            if "schema" in json_file.name.lower():
                continue

            checked_count += 1
            if not validate_fixture_freshness(json_file, strict=False):
                stale_count += 1

    if stale_count > 0:
        warnings.warn(
            f"Found {stale_count} potentially stale fixtures out of {checked_count} checked. "
            f"Run 'pytest --fixtures-audit' for details.",
            UserWarning,
        )

    yield

    # Cleanup
    _validated_fixtures.clear()


@pytest.fixture
def response_validator():
    """Fixture providing response envelope validation function.

    Usage:
        def test_my_tool(response_validator):
            response = my_tool()
            response_validator(response)  # Raises on invalid
    """
    return validate_response_envelope


@pytest.fixture
def fixture_freshness_checker():
    """Fixture providing fixture freshness validation function.

    Usage:
        def test_with_fixture(fixture_freshness_checker, tmp_path):
            fixture_path = tmp_path / "data.json"
            # ... create fixture ...
            fixture_freshness_checker(fixture_path)  # Warns on stale
    """
    return validate_fixture_freshness


@pytest.fixture
def assert_response_contract():
    """Fixture for asserting response contract compliance.

    Provides a callable that validates and returns the response,
    allowing chained assertions.

    Usage:
        def test_tool_response(assert_response_contract):
            response = my_tool()
            validated = assert_response_contract(response)
            assert validated["data"]["count"] == 5
    """
    def _assert(response: Dict[str, Any]) -> Dict[str, Any]:
        validate_response_envelope(response)
        return response

    return _assert


# Check if pytest-asyncio is available
try:
    import pytest_asyncio
    HAS_PYTEST_ASYNCIO = True
except ImportError:
    HAS_PYTEST_ASYNCIO = False


# Configure pytest to recognize our custom markers
def pytest_configure(config):
    """Register custom markers."""
    config.addinivalue_line(
        "markers",
        "fixture_version(version): mark test with expected fixture version",
    )
    config.addinivalue_line(
        "markers",
        "requires_fresh_fixtures: mark test that requires up-to-date fixtures",
    )
    config.addinivalue_line(
        "markers",
        "asyncio: mark test as async (requires pytest-asyncio)",
    )


def pytest_collection_modifyitems(config, items):
    """Modify test collection based on fixture requirements and async support."""
    skip_asyncio = pytest.mark.skip(reason="pytest-asyncio not installed")

    for item in items:
        # Skip async tests when pytest-asyncio is not available
        if not HAS_PYTEST_ASYNCIO and item.get_closest_marker("asyncio"):
            item.add_marker(skip_asyncio)

        # Check for requires_fresh_fixtures marker
        if item.get_closest_marker("requires_fresh_fixtures"):
            # These tests will fail-fast if any fixtures are stale
            item.add_marker(
                pytest.mark.skipif(
                    len(_validated_fixtures) == 0,
                    reason="Fixture validation not yet run",
                )
            )
