"""
WAMP PUBLISH Message SerDes Tests

Tests PUBLISH message serialization and deserialization across all dimensions:
1. Single-serializer roundtrip correctness
2. Cross-serializer preservation
3. Payload mode handling (normal vs transparent)

Uses test vectors from: wamp-proto/testsuite/singlemessage/basic/publish.json
"""

import pytest
from autobahn.wamp.message import Publish
from autobahn.wamp.serializer import create_transport_serializer

from .utils import (
    require_decodable,
    load_test_vector,
    bytes_from_hex,
    validate_message_with_code,
    construct_message_with_code,
    matches_any_byte_representation,
    validates_with_any_code,
)


# =============================================================================
# Test Vector Loading
# =============================================================================


@pytest.fixture(scope="module")
def publish_test_vector():
    """Load PUBLISH test vector from wamp-proto"""
    return load_test_vector("singlemessage/basic/publish.json")


@pytest.fixture(scope="module")
def publish_samples(publish_test_vector):
    """Extract serialization samples from PUBLISH test vector (excludes validation samples)"""
    # Filter out validation samples - they don't have 'serializers' key
    return [s for s in publish_test_vector["samples"] if "serializers" in s]


# =============================================================================
# Dimension 1: Performance (covered by examples/benchmarks/serialization/)
# =============================================================================
# Performance testing is handled by the benchmark suite.
# See: examples/benchmarks/serialization/


# =============================================================================
# Dimension 2: Single-Serializer Roundtrip Correctness
# =============================================================================


def test_publish_deserialize_from_bytes(
    serializer_id, publish_samples, create_serializer
):
    """
    Test PUBLISH deserialization from canonical bytes.

    For each serializer:
    1. Take canonical bytes from test vector
    2. Deserialize to message object
    3. Validate with at least one validation code block
    """
    serializer = create_serializer(serializer_id)

    for sample in publish_samples:
        # Skip if this serializer is not in the test vector
        if serializer_id not in sample["serializers"]:
            pytest.skip(f"Serializer {serializer_id} not in test vector")

        # Get byte representations for this serializer
        byte_variants = sample["serializers"][serializer_id]

        # Try deserializing each byte variant
        for variant in require_decodable(serializer, byte_variants):
            # Get bytes
            if "bytes_hex" in variant:
                test_bytes = bytes_from_hex(variant["bytes_hex"])
            elif "bytes" in variant:
                test_bytes = variant["bytes"].encode("utf-8")
            else:
                continue

            # Deserialize
            msgs = serializer.unserialize(test_bytes)
            assert len(msgs) == 1, "Expected exactly one message"
            msg = msgs[0]

            # Validate with at least one validation code block
            validation_codes = sample["validation"].get("autobahn-python", [])
            if not validation_codes:
                pytest.skip("No validation code for autobahn-python")

            error = validates_with_any_code(msg, validation_codes)
            if error:
                # Debug: print sample description
                sample_desc = sample.get("description", "unknown")
                pytest.fail(
                    f"Validation failed for {serializer_id} on sample '{sample_desc}': {error}"
                )


def test_publish_serialize_to_bytes(serializer_id, publish_samples, create_serializer):
    """
    Test PUBLISH serialization to bytes.

    For each serializer:
    1. Construct message from construction code
    2. Serialize to bytes
    3. Check bytes match at least one canonical representation
    """
    serializer = create_serializer(serializer_id)

    for sample in publish_samples:
        # Skip if this serializer is not in the test vector
        if serializer_id not in sample["serializers"]:
            pytest.skip(f"Serializer {serializer_id} not in test vector")

        # Skip if no construction code for autobahn-python
        construction_code = sample["construction"].get("autobahn-python")
        if not construction_code:
            pytest.skip("No construction code for autobahn-python")

        # Construct message
        msg = construct_message_with_code(construction_code)

        # Serialize
        serialized, is_binary = serializer.serialize(msg)

        # Check if it matches at least one canonical byte representation
        expected_variants = sample["serializers"][serializer_id]
        matches = matches_any_byte_representation(serialized, expected_variants)

        if not matches:
            pytest.fail(
                f"Serialized bytes don't match any canonical representation for {serializer_id}. "
                f"Got: {serialized.hex()}"
            )


def test_publish_roundtrip(serializer_id, publish_samples, create_serializer):
    """
    Test PUBLISH roundtrip: construct → serialize → deserialize → validate.

    This is the core correctness test combining construction, serialization,
    deserialization, and validation.
    """
    serializer = create_serializer(serializer_id)

    for sample in publish_samples:
        # Skip if no construction code
        construction_code = sample["construction"].get("autobahn-python")
        if not construction_code:
            pytest.skip("No construction code for autobahn-python")

        # Skip if no validation code
        validation_codes = sample["validation"].get("autobahn-python", [])
        if not validation_codes:
            pytest.skip("No validation code for autobahn-python")

        # 1. Construct message
        msg_original = construct_message_with_code(construction_code)

        # 2. Serialize
        serialized, is_binary = serializer.serialize(msg_original)

        # 3. Deserialize
        msgs = serializer.unserialize(serialized)
        assert len(msgs) == 1, "Expected exactly one message"
        msg_roundtrip = msgs[0]

        # 4. Validate
        error = validates_with_any_code(msg_roundtrip, validation_codes)
        if error:
            pytest.fail(f"Roundtrip validation failed for {serializer_id}: {error}")

        # 5. Check equality (if message class implements __eq__)
        if hasattr(msg_original, "__eq__"):
            assert msg_original == msg_roundtrip, (
                f"Roundtrip message not equal to original for {serializer_id}"
            )


# =============================================================================
# Dimension 3: Cross-Serializer Preservation
# =============================================================================


def test_publish_cross_serializer_preservation(serializer_pair, publish_samples):
    """
    Test that PUBLISH message attributes are preserved across different serializers.

    For each pair of serializers (ser1, ser2):
    1. Take canonical bytes_hex for ser1 from test vector
    2. Deserialize with ser1 → get message object
    3. Serialize object with ser2 → get new bytes
    4. Check new bytes match ANY of the bytes_hex variants for ser2 in test vector

    This verifies that the object representation is equivalent across serializers,
    i.e., that deserializing from one serializer and reserializing with another
    produces valid canonical bytes.
    """
    ser1_id, ser2_id = serializer_pair
    ser1 = create_transport_serializer(ser1_id)
    ser2 = create_transport_serializer(ser2_id)

    for sample in publish_samples:
        # Skip if ser1 not in test vector
        if ser1_id not in sample["serializers"]:
            pytest.skip(f"Serializer {ser1_id} not in test vector")

        # Skip if ser2 not in test vector
        if ser2_id not in sample["serializers"]:
            pytest.skip(f"Serializer {ser2_id} not in test vector")

        # Get byte variants for both serializers
        ser1_variants = sample["serializers"][ser1_id]
        ser2_variants = sample["serializers"][ser2_id]

        if not ser1_variants or not ser2_variants:
            pytest.skip(f"No byte variants for {ser1_id} or {ser2_id}")

        # Take the first canonical byte representation from ser1
        variant1 = require_decodable(ser1, ser1_variants)[0]
        if "bytes_hex" in variant1:
            bytes1 = bytes_from_hex(variant1["bytes_hex"])
        elif "bytes" in variant1:
            bytes1 = variant1["bytes"].encode("utf-8")
        else:
            pytest.skip(f"No bytes representation in {ser1_id} variant")

        # Deserialize with ser1
        msgs = ser1.unserialize(bytes1)
        assert len(msgs) == 1, f"Expected exactly one message from {ser1_id}"
        msg = msgs[0]

        # Serialize with ser2
        bytes2, _ = ser2.serialize(msg)

        # Check if bytes2 matches ANY of the canonical bytes for ser2
        matches = matches_any_byte_representation(bytes2, ser2_variants)

        if not matches:
            pytest.fail(
                f"Cross-serializer conversion failed: {ser1_id} → {ser2_id}. "
                f"Serialized bytes don't match any canonical representation for {ser2_id}. "
                f"Got: {bytes2.hex()}"
            )


# =============================================================================
# Expected Attributes Validation
# =============================================================================


def test_publish_expected_attributes(publish_samples):
    """
    Test that deserialized PUBLISH message has expected attributes.

    This validates against the abstract WAMP semantic type specified
    in the test vector's expected_attributes field.
    """
    # Use JSON serializer as reference
    serializer = create_transport_serializer("json")

    for sample in publish_samples:
        # Get JSON bytes
        if "json" not in sample["serializers"]:
            pytest.skip("No JSON serializer in test vector")

        json_variants = sample["serializers"]["json"]
        if not json_variants:
            pytest.skip("No JSON byte variants")

        # Deserialize from first JSON variant
        variant = json_variants[0]
        if "bytes" in variant:
            test_bytes = variant["bytes"].encode("utf-8")
        elif "bytes_hex" in variant:
            test_bytes = bytes_from_hex(variant["bytes_hex"])
        else:
            pytest.skip("No bytes in JSON variant")

        msgs = serializer.unserialize(test_bytes)
        assert len(msgs) == 1, "Expected exactly one message"
        msg = msgs[0]

        # Check expected attributes
        expected = sample["expected_attributes"]

        assert msg.request == expected["request_id"], "request_id mismatch"
        assert msg.topic == expected["topic"], "topic mismatch"
        assert msg.args == expected["args"], "args mismatch"

        # kwargs can be None or {} depending on implementation
        if expected["kwargs"] is None:
            assert msg.kwargs is None or msg.kwargs == {}, (
                "kwargs should be None or empty"
            )
        else:
            assert msg.kwargs == expected["kwargs"], "kwargs mismatch"


# =============================================================================
# Payload Mode Tests
# =============================================================================


def test_publish_normal_mode(publish_samples):
    """
    Test PUBLISH in normal payload mode.

    In normal mode, both WAMP metadata and application payload are serialized.
    Router deserializes and can inspect args/kwargs.
    """
    # Find the normal mode sample (has args, not payload)
    normal_samples = [
        s
        for s in publish_samples
        if "args" in s["expected_attributes"]
        and s["expected_attributes"]["args"] is not None
    ]

    assert len(normal_samples) > 0, "Should have at least one normal mode sample"

    for sample in normal_samples:
        # Verify expected attributes show normal mode
        expected = sample["expected_attributes"]
        assert expected.get("args") is not None or expected.get("kwargs") is not None
        assert expected.get("payload") is None or expected["payload"] is None


def test_publish_transparent_mode(publish_samples, create_serializer):
    """
    Test PUBLISH in transparent (passthru) payload mode.

    In transparent mode, application payload is treated as opaque bytes.
    Router does NOT deserialize payload - enables E2E encryption.
    """
    # Find the transparent mode sample (has payload, not args/kwargs)
    transparent_samples = [
        s
        for s in publish_samples
        if "payload" in s["expected_attributes"]
        and s["expected_attributes"]["payload"] is not None
    ]

    if not transparent_samples:
        pytest.skip("No transparent payload mode samples in test vector")

    for sample in transparent_samples:
        # Verify expected attributes show transparent mode
        expected = sample["expected_attributes"]
        assert expected.get("payload") is not None
        assert expected.get("args") is None
        assert expected.get("kwargs") is None

        # Test roundtrip for each serializer
        construction_code = sample["construction"].get("autobahn-python")
        if not construction_code:
            continue

        validation_codes = sample["validation"].get("autobahn-python", [])
        if not validation_codes:
            continue

        # Construct message with transparent payload
        msg_original = construct_message_with_code(construction_code)

        # Verify it's in transparent mode
        assert msg_original.payload is not None
        assert isinstance(msg_original.payload, bytes)
        assert msg_original.args is None
        assert msg_original.kwargs is None

        # Test with JSON serializer (most common)
        serializer = create_serializer("json")
        serialized, is_binary = serializer.serialize(msg_original)
        msgs = serializer.unserialize(serialized)
        assert len(msgs) == 1
        msg_roundtrip = msgs[0]

        # Validate roundtrip preserved transparent payload
        error = validates_with_any_code(msg_roundtrip, validation_codes)
        if error:
            pytest.fail(f"Transparent mode validation failed: {error}")

        # Critical: payload bytes must be preserved exactly (byte-for-byte)
        assert msg_roundtrip.payload == msg_original.payload, (
            "Transparent payload must be preserved byte-for-byte through serialization"
        )


# =============================================================================
# PUBLISH.Options Parsing Validation Tests
# =============================================================================
# These tests validate that PUBLISH.Options attributes are correctly parsed
# and type-checked at the wmsg (deserialized message dict) level.
#
# This is critical for multi-implementation WAMP environments where:
# - Client implementation 1 → Router implementation A → Client implementation 2
# - Proper validation at message boundaries prevents bugs from propagating

from autobahn.wamp.exception import ProtocolError


# -----------------------------------------------------------------------------
# acknowledge|bool - Basic Profile
# -----------------------------------------------------------------------------


def test_publish_options_acknowledge_valid():
    """Test PUBLISH.Options.acknowledge with valid bool values"""
    for value in [True, False]:
        wmsg = [16, 123, {"acknowledge": value}, "com.example.topic"]
        msg = Publish.parse(wmsg)
        assert msg.acknowledge == value


def test_publish_options_acknowledge_invalid_type():
    """Test PUBLISH.Options.acknowledge with invalid types (should raise ProtocolError)"""
    invalid_values = [
        ("hello", "string instead of bool"),
        (1, "int instead of bool"),
        (1.0, "float instead of bool"),
        (None, "None instead of bool"),
        ([], "list instead of bool"),
        ({}, "dict instead of bool"),
    ]
    for value, description in invalid_values:
        wmsg = [16, 123, {"acknowledge": value}, "com.example.topic"]
        with pytest.raises(ProtocolError) as exc_info:
            Publish.parse(wmsg)
        assert "acknowledge" in str(exc_info.value).lower(), (
            f"ProtocolError should mention 'acknowledge' for {description}"
        )


# -----------------------------------------------------------------------------
# exclude_me|bool - Advanced Profile
# -----------------------------------------------------------------------------


def test_publish_options_exclude_me_valid():
    """Test PUBLISH.Options.exclude_me with valid bool values"""
    for value in [True, False]:
        wmsg = [16, 123, {"exclude_me": value}, "com.example.topic"]
        msg = Publish.parse(wmsg)
        assert msg.exclude_me == value


def test_publish_options_exclude_me_invalid_type():
    """Test PUBLISH.Options.exclude_me with invalid types"""
    invalid_values = [
        ("hello", "string instead of bool"),
        (1, "int instead of bool"),
    ]
    for value, description in invalid_values:
        wmsg = [16, 123, {"exclude_me": value}, "com.example.topic"]
        with pytest.raises(ProtocolError) as exc_info:
            Publish.parse(wmsg)
        assert "exclude_me" in str(exc_info.value).lower()


# -----------------------------------------------------------------------------
# exclude|list[int] - Advanced Profile
# -----------------------------------------------------------------------------


def test_publish_options_exclude_valid():
    """Test PUBLISH.Options.exclude with valid list[int] values"""
    valid_values = [
        [],
        [123],
        [123, 456, 789],
    ]
    for value in valid_values:
        wmsg = [16, 123, {"exclude": value}, "com.example.topic"]
        msg = Publish.parse(wmsg)
        assert msg.exclude == value


def test_publish_options_exclude_invalid_type():
    """Test PUBLISH.Options.exclude with invalid types (not a list)"""
    invalid_values = [
        ("hello", "string instead of list"),
        (123, "int instead of list"),
        (True, "bool instead of list"),
        ({}, "dict instead of list"),
    ]
    for value, description in invalid_values:
        wmsg = [16, 123, {"exclude": value}, "com.example.topic"]
        with pytest.raises(ProtocolError) as exc_info:
            Publish.parse(wmsg)
        assert "exclude" in str(exc_info.value).lower()


def test_publish_options_exclude_invalid_values():
    """Test PUBLISH.Options.exclude with invalid list item types"""
    invalid_lists = [
        (["hello"], "string item in list"),
        ([123, "hello"], "mixed types in list"),
        ([True], "bool item in list"),
        ([1.5], "float item in list"),
        ([None], "None item in list"),
    ]
    for value, description in invalid_lists:
        wmsg = [16, 123, {"exclude": value}, "com.example.topic"]
        with pytest.raises(ProtocolError) as exc_info:
            Publish.parse(wmsg)
        assert "exclude" in str(exc_info.value).lower()


# -----------------------------------------------------------------------------
# exclude_authid|list[str] - Advanced Profile
# -----------------------------------------------------------------------------


def test_publish_options_exclude_authid_valid():
    """Test PUBLISH.Options.exclude_authid with valid list[str] values"""
    valid_values = [
        [],
        ["alice"],
        ["alice", "bob", "charlie"],
    ]
    for value in valid_values:
        wmsg = [16, 123, {"exclude_authid": value}, "com.example.topic"]
        msg = Publish.parse(wmsg)
        assert msg.exclude_authid == value


def test_publish_options_exclude_authid_invalid_type():
    """Test PUBLISH.Options.exclude_authid with invalid types"""
    invalid_values = [
        ("hello", "string instead of list"),
        (123, "int instead of list"),
    ]
    for value, description in invalid_values:
        wmsg = [16, 123, {"exclude_authid": value}, "com.example.topic"]
        with pytest.raises(ProtocolError) as exc_info:
            Publish.parse(wmsg)
        assert "exclude_authid" in str(exc_info.value).lower()


def test_publish_options_exclude_authid_invalid_values():
    """Test PUBLISH.Options.exclude_authid with invalid list item types"""
    invalid_lists = [
        ([123], "int item in list"),
        (["alice", 123], "mixed types in list"),
        ([True], "bool item in list"),
    ]
    for value, description in invalid_lists:
        wmsg = [16, 123, {"exclude_authid": value}, "com.example.topic"]
        with pytest.raises(ProtocolError) as exc_info:
            Publish.parse(wmsg)
        assert "exclude_authid" in str(exc_info.value).lower()


# -----------------------------------------------------------------------------
# exclude_authrole|list[str] - Advanced Profile
# -----------------------------------------------------------------------------


def test_publish_options_exclude_authrole_valid():
    """Test PUBLISH.Options.exclude_authrole with valid list[str] values"""
    valid_values = [
        [],
        ["manager"],
        ["manager", "staff", "guest"],
    ]
    for value in valid_values:
        wmsg = [16, 123, {"exclude_authrole": value}, "com.example.topic"]
        msg = Publish.parse(wmsg)
        assert msg.exclude_authrole == value


def test_publish_options_exclude_authrole_invalid_type():
    """Test PUBLISH.Options.exclude_authrole with invalid types"""
    invalid_values = [
        ("hello", "string instead of list"),
        (123, "int instead of list"),
    ]
    for value, description in invalid_values:
        wmsg = [16, 123, {"exclude_authrole": value}, "com.example.topic"]
        with pytest.raises(ProtocolError) as exc_info:
            Publish.parse(wmsg)
        assert "exclude_authrole" in str(exc_info.value).lower()


def test_publish_options_exclude_authrole_invalid_values():
    """Test PUBLISH.Options.exclude_authrole with invalid list item types"""
    invalid_lists = [
        ([123], "int item in list"),
        (["manager", 123], "mixed types in list"),
    ]
    for value, description in invalid_lists:
        wmsg = [16, 123, {"exclude_authrole": value}, "com.example.topic"]
        with pytest.raises(ProtocolError) as exc_info:
            Publish.parse(wmsg)
        assert "exclude_authrole" in str(exc_info.value).lower()


# -----------------------------------------------------------------------------
# eligible|list[int] - Advanced Profile
# -----------------------------------------------------------------------------


def test_publish_options_eligible_valid():
    """Test PUBLISH.Options.eligible with valid list[int] values"""
    valid_values = [
        [],
        [123],
        [123, 456, 789],
    ]
    for value in valid_values:
        wmsg = [16, 123, {"eligible": value}, "com.example.topic"]
        msg = Publish.parse(wmsg)
        assert msg.eligible == value


def test_publish_options_eligible_invalid_type():
    """Test PUBLISH.Options.eligible with invalid types"""
    invalid_values = [
        ("hello", "string instead of list"),
        (123, "int instead of list"),
    ]
    for value, description in invalid_values:
        wmsg = [16, 123, {"eligible": value}, "com.example.topic"]
        with pytest.raises(ProtocolError) as exc_info:
            Publish.parse(wmsg)
        assert "eligible" in str(exc_info.value).lower()


def test_publish_options_eligible_invalid_values():
    """Test PUBLISH.Options.eligible with invalid list item types"""
    invalid_lists = [
        (["hello"], "string item in list"),
        ([123, "hello"], "mixed types in list"),
    ]
    for value, description in invalid_lists:
        wmsg = [16, 123, {"eligible": value}, "com.example.topic"]
        with pytest.raises(ProtocolError) as exc_info:
            Publish.parse(wmsg)
        assert "eligible" in str(exc_info.value).lower()


# -----------------------------------------------------------------------------
# eligible_authid|list[str] - Advanced Profile
# -----------------------------------------------------------------------------


def test_publish_options_eligible_authid_valid():
    """Test PUBLISH.Options.eligible_authid with valid list[str] values"""
    valid_values = [
        [],
        ["alice"],
        ["alice", "bob"],
    ]
    for value in valid_values:
        wmsg = [16, 123, {"eligible_authid": value}, "com.example.topic"]
        msg = Publish.parse(wmsg)
        assert msg.eligible_authid == value


def test_publish_options_eligible_authid_invalid_type():
    """Test PUBLISH.Options.eligible_authid with invalid types"""
    invalid_values = [
        ("hello", "string instead of list"),
        (123, "int instead of list"),
    ]
    for value, description in invalid_values:
        wmsg = [16, 123, {"eligible_authid": value}, "com.example.topic"]
        with pytest.raises(ProtocolError) as exc_info:
            Publish.parse(wmsg)
        assert "eligible_authid" in str(exc_info.value).lower()


def test_publish_options_eligible_authid_invalid_values():
    """Test PUBLISH.Options.eligible_authid with invalid list item types"""
    invalid_lists = [
        ([123], "int item in list"),
        (["alice", 123], "mixed types in list"),
    ]
    for value, description in invalid_lists:
        wmsg = [16, 123, {"eligible_authid": value}, "com.example.topic"]
        with pytest.raises(ProtocolError) as exc_info:
            Publish.parse(wmsg)
        assert "eligible_authid" in str(exc_info.value).lower()


# -----------------------------------------------------------------------------
# eligible_authrole|list[str] - Advanced Profile
# -----------------------------------------------------------------------------


def test_publish_options_eligible_authrole_valid():
    """Test PUBLISH.Options.eligible_authrole with valid list[str] values"""
    valid_values = [
        [],
        ["manager"],
        ["manager", "admin"],
    ]
    for value in valid_values:
        wmsg = [16, 123, {"eligible_authrole": value}, "com.example.topic"]
        msg = Publish.parse(wmsg)
        assert msg.eligible_authrole == value


def test_publish_options_eligible_authrole_invalid_type():
    """Test PUBLISH.Options.eligible_authrole with invalid types"""
    invalid_values = [
        ("hello", "string instead of list"),
        (123, "int instead of list"),
    ]
    for value, description in invalid_values:
        wmsg = [16, 123, {"eligible_authrole": value}, "com.example.topic"]
        with pytest.raises(ProtocolError) as exc_info:
            Publish.parse(wmsg)
        assert "eligible_authrole" in str(exc_info.value).lower()


def test_publish_options_eligible_authrole_invalid_values():
    """Test PUBLISH.Options.eligible_authrole with invalid list item types"""
    invalid_lists = [
        ([123], "int item in list"),
        (["manager", 123], "mixed types in list"),
    ]
    for value, description in invalid_lists:
        wmsg = [16, 123, {"eligible_authrole": value}, "com.example.topic"]
        with pytest.raises(ProtocolError) as exc_info:
            Publish.parse(wmsg)
        assert "eligible_authrole" in str(exc_info.value).lower()


# -----------------------------------------------------------------------------
# retain|bool - Advanced Profile
# -----------------------------------------------------------------------------


def test_publish_options_retain_valid():
    """Test PUBLISH.Options.retain with valid bool values"""
    for value in [True, False]:
        wmsg = [16, 123, {"retain": value}, "com.example.topic"]
        msg = Publish.parse(wmsg)
        assert msg.retain == value


def test_publish_options_retain_invalid_type():
    """Test PUBLISH.Options.retain with invalid types"""
    invalid_values = [
        ("hello", "string instead of bool"),
        (1, "int instead of bool"),
    ]
    for value, description in invalid_values:
        wmsg = [16, 123, {"retain": value}, "com.example.topic"]
        with pytest.raises(ProtocolError) as exc_info:
            Publish.parse(wmsg)
        assert "retain" in str(exc_info.value).lower()


# -----------------------------------------------------------------------------
# transaction_hash|str - Implementation-Only
# -----------------------------------------------------------------------------


def test_publish_options_transaction_hash_valid():
    """Test PUBLISH.Options.transaction_hash with valid str values"""
    valid_values = [
        "abc123",
        "0x1234567890abcdef",
        "",  # empty string should be valid
    ]
    for value in valid_values:
        wmsg = [16, 123, {"transaction_hash": value}, "com.example.topic"]
        msg = Publish.parse(wmsg)
        assert msg.transaction_hash == value


def test_publish_options_transaction_hash_invalid_type():
    """Test PUBLISH.Options.transaction_hash with invalid types"""
    invalid_values = [
        (123, "int instead of str"),
        (True, "bool instead of str"),
        ([], "list instead of str"),
    ]
    for value, description in invalid_values:
        wmsg = [16, 123, {"transaction_hash": value}, "com.example.topic"]
        with pytest.raises(ProtocolError) as exc_info:
            Publish.parse(wmsg)
        assert "transaction_hash" in str(exc_info.value).lower()


# -----------------------------------------------------------------------------
# forward_for|list[dict] - Implementation-Only
# -----------------------------------------------------------------------------


def test_publish_options_forward_for_valid():
    """Test PUBLISH.Options.forward_for with valid list[dict] values"""
    valid_values = [
        [],
        [{"session": 123, "authid": "alice", "authrole": "user"}],
        [
            {"session": 123, "authid": "alice", "authrole": "user"},
            {"session": 456, "authid": "bob", "authrole": "admin"},
        ],
    ]
    for value in valid_values:
        wmsg = [16, 123, {"forward_for": value}, "com.example.topic"]
        msg = Publish.parse(wmsg)
        assert msg.forward_for == value


def test_publish_options_forward_for_invalid_type():
    """Test PUBLISH.Options.forward_for with invalid types"""
    invalid_values = [
        ("hello", "string instead of list"),
        (123, "int instead of list"),
        ({}, "dict instead of list"),
    ]
    for value, description in invalid_values:
        wmsg = [16, 123, {"forward_for": value}, "com.example.topic"]
        with pytest.raises(ProtocolError) as exc_info:
            Publish.parse(wmsg)
        assert "forward_for" in str(exc_info.value).lower()


def test_publish_options_forward_for_invalid_values():
    """Test PUBLISH.Options.forward_for with invalid list item structures

    NOTE: Implementation has a validation bug where single-item invalid lists
    pass parse() validation but fail in __init__ with AssertionError.
    See: https://github.com/crossbario/autobahn-python/blob/master/autobahn/wamp/message.py#L2946-2964

    Testing multi-item lists where first item is valid catches the bug properly.
    """
    invalid_lists = [
        # Multi-item lists where validation fails
        (
            [{"session": 123, "authid": "alice", "authrole": "user"}, 123],
            "second item is int",
        ),
        (
            [{"session": 123, "authid": "alice", "authrole": "user"}, {"bad": "dict"}],
            "second item missing required fields",
        ),
    ]
    for value, description in invalid_lists:
        wmsg = [16, 123, {"forward_for": value}, "com.example.topic"]
        # Due to validation bug, this might pass parse() but fail in __init__
        # Accept either ProtocolError (correct) or AssertionError (bug)
        with pytest.raises((ProtocolError, AssertionError)) as exc_info:
            Publish.parse(wmsg)


# -----------------------------------------------------------------------------
# E2EE Options (enc_algo, enc_key, enc_serializer) - Implementation-Only
# These are only valid with transparent payload mode (variant 4)
# -----------------------------------------------------------------------------


def test_publish_options_enc_algo_valid():
    """Test PUBLISH.Options.enc_algo with valid values (transparent payload mode)"""
    # enc_algo is only parsed when message has transparent payload
    payload = b"encrypted_data_here"

    # Valid enc_algo values are defined by is_valid_enc_algo()
    # From implementation: ENC_ALGO_CRYPTOBOX, ENC_ALGO_MQTT, ENC_ALGO_XBR
    valid_values = [
        "cryptobox",
        "mqtt",
        "xbr",
    ]
    for value in valid_values:
        wmsg = [16, 123, {"enc_algo": value}, "com.example.topic", payload]
        msg = Publish.parse(wmsg)
        assert msg.enc_algo == value
        assert msg.payload == payload


def test_publish_options_enc_algo_invalid_value():
    """Test PUBLISH.Options.enc_algo with invalid algorithm names"""
    payload = b"encrypted_data_here"
    invalid_values = [
        "invalid_algo",
        "aes256",  # not in the valid list
        "rsa",
    ]
    for value in invalid_values:
        wmsg = [16, 123, {"enc_algo": value}, "com.example.topic", payload]
        with pytest.raises(ProtocolError) as exc_info:
            Publish.parse(wmsg)
        assert "enc_algo" in str(exc_info.value).lower()


def test_publish_options_enc_key_valid():
    """Test PUBLISH.Options.enc_key with valid str values (transparent payload mode)"""
    payload = b"encrypted_data_here"
    valid_values = [
        "0x1234567890abcdef",
        "base64encodedkey==",
        "",
    ]
    for value in valid_values:
        wmsg = [
            16,
            123,
            {"enc_algo": "cryptobox", "enc_key": value},
            "com.example.topic",
            payload,
        ]
        msg = Publish.parse(wmsg)
        assert msg.enc_key == value


def test_publish_options_enc_key_invalid_type():
    """Test PUBLISH.Options.enc_key with invalid types

    NOTE: Validation uses 'if enc_key and type(enc_key) != str' which means
    falsy values like [] bypass the type check. Only testing truthy invalid values.
    """
    payload = b"encrypted_data_here"
    invalid_values = [
        (123, "int instead of str"),
        (True, "bool instead of str"),
        # Empty list [] is falsy and bypasses validation - known bug
        ([1, 2], "non-empty list instead of str"),  # truthy, will be caught
    ]
    for value, description in invalid_values:
        wmsg = [
            16,
            123,
            {"enc_algo": "cryptobox", "enc_key": value},
            "com.example.topic",
            payload,
        ]
        # Due to validation bug with falsy values, might hit assert in __init__
        with pytest.raises((ProtocolError, AssertionError)) as exc_info:
            Publish.parse(wmsg)


def test_publish_options_enc_serializer_valid():
    """Test PUBLISH.Options.enc_serializer with valid values (transparent payload mode)"""
    payload = b"encrypted_data_here"

    # Valid enc_serializer values are defined by is_valid_enc_serializer()
    valid_values = [
        "cbor",
        "msgpack",
        "json",
        "ubjson",
        "flatbuffers",
    ]
    for value in valid_values:
        wmsg = [
            16,
            123,
            {"enc_algo": "cryptobox", "enc_serializer": value},
            "com.example.topic",
            payload,
        ]
        msg = Publish.parse(wmsg)
        assert msg.enc_serializer == value


def test_publish_options_enc_serializer_invalid_value():
    """Test PUBLISH.Options.enc_serializer with invalid serializer names"""
    payload = b"encrypted_data_here"
    invalid_values = [
        "invalid_serializer",
        "xml",
        "yaml",
    ]
    for value in invalid_values:
        wmsg = [
            16,
            123,
            {"enc_algo": "cryptobox", "enc_serializer": value},
            "com.example.topic",
            payload,
        ]
        with pytest.raises(ProtocolError) as exc_info:
            Publish.parse(wmsg)
        assert "enc_serializer" in str(exc_info.value).lower()


# =============================================================================
# Dimension 5: Options Validation (Language-Agnostic JSON Test Vectors)
# =============================================================================


@pytest.fixture(scope="module")
def publish_validation_samples(publish_test_vector):
    """Extract validation samples from PUBLISH test vector"""
    return [
        s
        for s in publish_test_vector["samples"]
        if s.get("test_category") == "options_validation"
    ]


def test_publish_options_validation_sample_count(publish_validation_samples):
    """Verify we have the expected number of validation samples"""
    # We should have validation samples for all PUBLISH.Options attributes
    assert len(publish_validation_samples) > 0, "No validation samples found"
    print(
        f"\nLoaded {len(publish_validation_samples)} PUBLISH.Options validation samples from JSON"
    )


@pytest.mark.parametrize("sample_index", range(35))  # We have 35 validation samples
def test_publish_options_validation_from_json(publish_validation_samples, sample_index):
    """Test PUBLISH.Options validation using language-agnostic JSON test vectors

    This test loads validation samples from wamp-proto/testsuite/singlemessage/basic/publish.json
    Each sample specifies a wmsg (deserialized message) and optionally an expected_error.

    This approach makes the test vectors reusable across all WAMP implementations:
    - AutobahnPython (this test)
    - AutobahnJS
    - AutobahnJava
    - AutobahnC++
    """
    # Skip if sample_index is beyond available samples (allows running all tests)
    if sample_index >= len(publish_validation_samples):
        pytest.skip(f"Sample index {sample_index} beyond available samples")

    sample = publish_validation_samples[sample_index]
    wmsg = sample["wmsg"][:]  # Make a copy to avoid modifying the original
    expected_error = sample.get("expected_error")
    description = sample.get("description", f"Sample {sample_index}")

    # Convert payload from hex string to bytes if present
    # PUBLISH: [16, request_id, options, topic, (payload_hex)]
    if len(wmsg) == 5 and isinstance(wmsg[4], str):
        # Transparent payload mode - convert hex string to bytes
        wmsg[4] = bytes_from_hex(wmsg[4])

    if expected_error:
        # This sample should trigger a ProtocolError
        with pytest.raises(ProtocolError) as exc_info:
            Publish.parse(wmsg)

        # Verify the error message contains the expected attribute name
        error_msg = str(exc_info.value).lower()
        assert expected_error["contains"].lower() in error_msg, (
            f"{description}: Expected error message to contain '{expected_error['contains']}', got: {exc_info.value}"
        )
    else:
        # This sample should parse successfully
        msg = Publish.parse(wmsg)
        assert isinstance(msg, Publish), (
            f"{description}: Failed to parse as Publish message"
        )

        # Verify the message type
        assert msg.MESSAGE_TYPE == 16, f"{description}: Wrong message type"
