"""Schema parity tests between EventPayload/EventType and IngestEventPayload/IngestEventType.

Validates that the MCP-side emitter models (mcp_vm_blackbox.event_emitter) and the
dashboard-side ingest models (vm_dashboard.app) accept identical JSON payloads and
produce identical model_dump() output, enforcing ADR-002 (dashboard/MCP decoupling
with structural parity).
"""

from __future__ import annotations

from typing import Any

import pytest
from vm_dashboard.app import IngestEventPayload, IngestEventType

from mcp_vm_blackbox.event_emitter import EventPayload, EventType

# ---------------------------------------------------------------------------
# Sample payloads for parametrised parity tests
# ---------------------------------------------------------------------------

_SAMPLE_PAYLOADS: list[tuple[str, dict[str, Any]]] = [
    (
        "job_created",
        {
            "event_type": "job_created",
            "vm_name": "prod-vm",
            "job_id": "job-001",
            "data": {"status": "active", "created_by": "pilot"},
            "timestamp": "2026-03-20T12:00:00+00:00",
        },
    ),
    (
        "screenshot_captured",
        {
            "event_type": "screenshot_captured",
            "vm_name": "qa-windows",
            "job_id": "job-042",
            "data": {"return_type": "Image"},
            "timestamp": "2026-03-20T13:30:00+00:00",
        },
    ),
    (
        "fleet_status",
        {
            "event_type": "fleet_status",
            "vm_name": "",
            "job_id": "",
            "data": {"total_vms": 4, "running": 2},
            "timestamp": "2026-03-20T14:00:00+00:00",
        },
    ),
    (
        "step_added",
        {
            "event_type": "step_added",
            "vm_name": "build-vm",
            "job_id": "job-007",
            "data": {"step": "Install dependencies", "status": "completed"},
            "timestamp": "2026-03-20T15:00:00+00:00",
        },
    ),
    (
        "job_updated",
        {
            "event_type": "job_updated",
            "vm_name": "test-vm",
            "job_id": "job-099",
            "data": {"status": "paused"},
            "timestamp": "2026-03-20T16:00:00+00:00",
        },
    ),
    ("minimal_defaults", {"event_type": "vm_state_changed", "vm_name": "", "job_id": "", "data": {}, "timestamp": ""}),
]


# ---------------------------------------------------------------------------
# Test suite — model parity
# ---------------------------------------------------------------------------


class TestEventPayloadSchemaParity:
    """Validates that EventPayload and IngestEventPayload accept the same JSON.

    Scope: ADR-002 structural parity — the two models are defined independently
    but must remain in sync so that payloads POSTed by the MCP middleware are
    accepted without error by the dashboard ingest endpoint.

    Strategy: Parametrise over representative sample payloads and construct both
    models from the same dict, then compare model_dump() outputs field-by-field.
    """

    @pytest.mark.parametrize(("label", "payload_dict"), _SAMPLE_PAYLOADS, ids=[p[0] for p in _SAMPLE_PAYLOADS])
    def test_both_models_accept_same_json(self, label: str, payload_dict: dict[str, Any]) -> None:
        """Both models construct without validation errors from identical JSON.

        Tests: ADR-002 parity — same wire format accepted by both sides.
        How: Instantiate EventPayload and IngestEventPayload from the same dict,
             assert no ValidationError is raised for either.
        Why: If either model rejects a payload the other accepts, the MCP→dashboard
             integration breaks silently at runtime.
        """
        # Arrange / Act — construction must not raise
        emitter_model = EventPayload(**payload_dict)
        dashboard_model = IngestEventPayload(**payload_dict)

        # Assert — both constructed successfully (no exception means success)
        assert emitter_model is not None
        assert dashboard_model is not None

    @pytest.mark.parametrize(("label", "payload_dict"), _SAMPLE_PAYLOADS, ids=[p[0] for p in _SAMPLE_PAYLOADS])
    def test_model_dump_produces_identical_output(self, label: str, payload_dict: dict[str, Any]) -> None:
        """model_dump() output is identical for both models given the same input.

        Tests: Wire format equivalence — the JSON sent by the emitter is the same
               structure the dashboard model would serialise back out.
        How: Construct both models from the same dict, call model_dump() on each,
             assert the resulting dicts are equal.
        Why: Divergence in model_dump() output means the two models have different
             internal field definitions despite accepting the same input, which
             would cause subtle deserialization bugs at the dashboard ingest endpoint.
        """
        # Arrange
        emitter_model = EventPayload(**payload_dict)
        dashboard_model = IngestEventPayload(**payload_dict)

        # Act
        emitter_dump = emitter_model.model_dump()
        dashboard_dump = dashboard_model.model_dump()

        # Assert — normalize enum values to strings for comparison
        emitter_normalised = {**emitter_dump, "event_type": str(emitter_dump["event_type"])}
        dashboard_normalised = {**dashboard_dump, "event_type": str(dashboard_dump["event_type"])}
        assert emitter_normalised == dashboard_normalised, (
            f"model_dump() mismatch for payload '{label}':\n"
            f"  emitter:   {emitter_normalised}\n"
            f"  dashboard: {dashboard_normalised}"
        )


# ---------------------------------------------------------------------------
# Test suite — enum parity
# ---------------------------------------------------------------------------


class TestEventTypeEnumParity:
    """Validates that EventType and IngestEventType have identical members.

    Scope: ADR-002 enum parity — both enums must have the same member names
    and string values so that event routing works end-to-end.
    """

    def test_both_enums_have_identical_member_names(self) -> None:
        """EventType and IngestEventType have the same set of member names.

        Tests: Enum name parity.
        How: Compare the sets of member names (case-normalised to uppercase) for
             both enums.
        Why: A member present in one enum but absent in the other means some event
             types can be emitted but not received, or vice versa.
        """
        # Arrange
        emitter_names = {m.upper() for m in EventType.__members__}
        dashboard_names = {m.upper() for m in IngestEventType.__members__}

        # Assert
        assert emitter_names == dashboard_names, (
            f"Enum member name mismatch:\n"
            f"  emitter only:   {emitter_names - dashboard_names}\n"
            f"  dashboard only: {dashboard_names - emitter_names}"
        )

    def test_both_enums_have_identical_string_values(self) -> None:
        """EventType and IngestEventType members map to identical string values.

        Tests: Enum value parity — the wire-format strings must match.
        How: For each member in EventType, assert the same string value exists
             in IngestEventType.
        Why: The dashboard routes WebSocket broadcasts by event_type string value;
             a mismatch means the frontend receives events with unexpected type strings.
        """
        # Arrange
        emitter_values = {str(v) for v in EventType}
        dashboard_values = {str(v) for v in IngestEventType}

        # Assert
        assert emitter_values == dashboard_values, (
            f"Enum string value mismatch:\n"
            f"  emitter only:   {emitter_values - dashboard_values}\n"
            f"  dashboard only: {dashboard_values - emitter_values}"
        )

    @pytest.mark.parametrize("member_name", list(EventType.__members__))
    def test_each_emitter_member_value_matches_dashboard(self, member_name: str) -> None:
        """Each EventType member has the matching string value in IngestEventType.

        Tests: Per-member value parity for clear failure messages on divergence.
        How: For each EventType member, look up the matching member in IngestEventType
             by normalised name, compare string values.
        Why: Parametrised per-member tests pinpoint exactly which enum member drifted,
             rather than reporting a set difference.
        """
        # Arrange
        emitter_value = str(EventType[member_name])

        # Look up by value since member names differ in case (UPPER vs lower)
        dashboard_values = {str(v) for v in IngestEventType}

        # Assert
        assert emitter_value in dashboard_values, (
            f"EventType.{member_name} value {emitter_value!r} not found in IngestEventType values: "
            f"{sorted(dashboard_values)}"
        )
