"""
Utility functions for WAMP serdes tests
"""

import json
import os
from pathlib import Path
from typing import Dict, List, Any, Optional


def get_wamp_proto_path() -> Path:
    """
    Get path to wamp-proto repository.

    Tries multiple locations in order:
    1. Git submodule at autobahn-python/.proto (preferred for CI/CD)
    2. Sibling directory at ../wamp-proto (local development)

    Directory structure:
      ~/work/wamp/autobahn-python/.proto/         (submodule - CI)
      ~/work/wamp/autobahn-python/                (this repo)
      ~/work/wamp/wamp-proto/                     (sibling - local dev)
    """
    current_file = Path(__file__).resolve()
    # Go up from tests/ -> serdes/ -> examples/ -> autobahn-python/
    autobahn_root = current_file.parent.parent.parent.parent

    # Try 1: Check for .proto submodule (CI/CD and new developers)
    wamp_proto_submodule = autobahn_root / ".proto"
    if wamp_proto_submodule.exists() and (wamp_proto_submodule / "testsuite").exists():
        return wamp_proto_submodule

    # Try 2: Check for sibling directory (local development)
    wamp_proto_sibling = autobahn_root.parent / "wamp-proto"
    if wamp_proto_sibling.exists() and (wamp_proto_sibling / "testsuite").exists():
        return wamp_proto_sibling

    # Neither location found
    raise FileNotFoundError(
        f"wamp-proto repository not found. Tried:\n"
        f"  1. Submodule: {wamp_proto_submodule}\n"
        f"  2. Sibling:   {wamp_proto_sibling}\n\n"
        f"Please either:\n"
        f"  - Run 'git submodule update --init --recursive' to initialize submodules, OR\n"
        f"  - Clone wamp-proto as a sibling directory to autobahn-python"
    )

    return wamp_proto


def load_test_vector(relative_path: str) -> Dict[str, Any]:
    """
    Load a test vector JSON file from wamp-proto/testsuite/

    Args:
        relative_path: Path relative to testsuite/, e.g. "singlemessage/basic/publish.json"

    Returns:
        Parsed JSON test vector
    """
    current_file = Path(__file__).resolve()
    autobahn_root = current_file.parent.parent.parent.parent

    # Try multiple locations for the test vector file
    # This allows for graceful fallback when submodule is outdated
    locations = [
        autobahn_root / ".proto" / "testsuite" / relative_path,  # Submodule
        autobahn_root.parent / "wamp-proto" / "testsuite" / relative_path,  # Sibling
    ]

    for test_vector_path in locations:
        if test_vector_path.exists():
            with open(test_vector_path, "r") as f:
                return json.load(f)

    # Neither location has the file
    raise FileNotFoundError(
        f"Test vector not found: {relative_path}\n"
        f"Tried locations:\n" + "\n".join(f"  - {loc}" for loc in locations)
    )


def get_serializer_ids() -> List[str]:
    """
    Get list of all WAMP serializer IDs to test.

    Returns:
        List of serializer IDs (e.g., ["json", "msgpack", "cbor", ...])
    """
    from autobahn.wamp.serializer import SERID_TO_OBJSER

    return sorted(SERID_TO_OBJSER.keys())


def bytes_from_hex(hex_string: str) -> bytes:
    """Convert hex string to bytes"""
    return bytes.fromhex(hex_string)


def bytes_to_hex(data: bytes) -> str:
    """Convert bytes to hex string"""
    return data.hex()


def validate_message_with_code(msg: Any, validation_code: str) -> None:
    """
    Validate a message using embedded Python code.

    The validation code is executed with 'msg' in scope.
    Any assertion failures will raise AssertionError.

    Args:
        msg: Deserialized WAMP message object
        validation_code: Python code string with assertions
    """
    # Create namespace with msg available
    namespace = {"msg": msg}

    # Execute validation code
    try:
        exec(validation_code, namespace)
    except AssertionError as e:
        # Re-raise with line number context if possible
        if not str(e):
            # Empty assertion - add context
            import traceback

            tb_lines = traceback.format_exc().split("\n")
            # Find the line that failed
            for line in tb_lines:
                if "assert" in line.lower():
                    raise AssertionError(f"Assertion failed: {line.strip()}") from e
        raise


def construct_message_with_code(construction_code: str) -> Any:
    """
    Construct a WAMP message using embedded Python code.

    The construction code should create a variable named 'msg'.

    Args:
        construction_code: Python code string that creates 'msg'

    Returns:
        Constructed WAMP message object
    """
    namespace = {}

    # Execute construction code
    exec(construction_code, namespace)

    # Extract the constructed message
    if "msg" not in namespace:
        raise ValueError("Construction code must create a variable named 'msg'")

    return namespace["msg"]


def matches_any_byte_representation(
    actual_bytes: bytes, expected_variants: List[Dict[str, str]]
) -> bool:
    """
    Check if actual bytes match any of the expected byte representations.

    This implements the "at least one" semantics for non-bijective serialization.

    Args:
        actual_bytes: Serialized bytes to check
        expected_variants: List of byte representations (each with 'bytes' or 'bytes_hex')

    Returns:
        True if actual_bytes matches at least one expected variant
    """
    for variant in expected_variants:
        if "bytes_hex" in variant:
            expected = bytes_from_hex(variant["bytes_hex"])
            if actual_bytes == expected:
                return True

        if "bytes" in variant:
            # For JSON serializer, the 'bytes' field is the string representation
            # We need to encode it to bytes for comparison
            expected = variant["bytes"].encode("utf-8")
            if actual_bytes == expected:
                return True

    return False


def require_decodable(serializer: Any, variants: List[Dict[str, str]]) -> List[Dict[str, str]]:
    """
    Return the subset of byte variants that ``serializer`` can actually decode.

    This implements the "at least one" semantics for the deserialize direction.
    A serializer's variant list may include encodings produced by other WAMP
    implementations or library versions (for example, the legacy py-ubjson bytes
    vs. the new bjdata/BJData bytes for the "ubjson" serializer, which are not
    mutually decodable). Feeding a foreign encoding to this serializer either
    raises, or - worse - decodes to a *valid but wrong* message. Both cases are
    skipped rather than treated as failures.

    A variant is considered "this serializer's own" iff it decodes AND
    re-serializing the decoded message reproduces one of the canonical variants
    in the list. This keeps purely cosmetic variants (e.g. spaced vs. compact
    JSON, which all decode to the same message) while dropping foreign encodings.

    At least one variant MUST qualify, otherwise this is a genuine conformance
    failure and an AssertionError is raised.

    Args:
        serializer: A transport serializer (with ``serialize``/``unserialize``)
        variants: List of byte representations (each with 'bytes' or 'bytes_hex')

    Returns:
        The variants (in original order) that ``serializer`` decodes consistently
    """
    # Tier 1: variants whose decode re-serializes to one of the canonical
    # variants (cosmetic variants + this backend's own encoding). Tier 2 (fallback):
    # variants that merely decode without raising - used for serializers whose
    # re-serialization is not byte-canonical (e.g. flatbuffers), where Tier 1
    # would be empty even though the bytes are this backend's own.
    consistent = []
    decodes = []
    for variant in variants:
        if "bytes_hex" in variant:
            data = bytes_from_hex(variant["bytes_hex"])
        elif "bytes" in variant:
            data = variant["bytes"].encode("utf-8")
        else:
            continue
        try:
            msg = serializer.unserialize(data)[0]
        except Exception:
            continue
        decodes.append(variant)
        try:
            reserialized, _ = serializer.serialize(msg)
        except Exception:
            continue
        if matches_any_byte_representation(reserialized, variants):
            consistent.append(variant)
    result = consistent or decodes
    assert result, "no byte variant could be deserialized by this serializer"
    return result


def validates_with_any_code(msg: Any, validation_codes: List[str]) -> Optional[str]:
    """
    Try to validate message with any of the validation code blocks.

    This implements the "at least one" semantics for validation.

    Args:
        msg: Deserialized WAMP message object
        validation_codes: List of validation code strings

    Returns:
        None if validation passed with at least one code block,
        or error message string if all validations failed
    """
    errors = []

    for idx, code in enumerate(validation_codes):
        try:
            validate_message_with_code(msg, code)
            return None  # Success!
        except AssertionError as e:
            # Include code snippet for debugging
            code_preview = code[:200] + "..." if len(code) > 200 else code
            errors.append(f"Validation block {idx}: {e}\n  Code: {code_preview}")
        except Exception as e:
            code_preview = code[:200] + "..." if len(code) > 200 else code
            errors.append(f"Validation block {idx} error: {e}\n  Code: {code_preview}")

    # All validation attempts failed
    return "; ".join(errors)
