from __future__ import annotations

from dataclasses import replace

import pytest

from k1link.simulation.contracts import ControlProfile
from k1link.simulation.provider_contract import (
    ProviderRole,
    SimulationClockDescriptor,
    SimulationProviderContractError,
    SimulationProviderDescriptor,
    SimulationProviderProfile,
)
from k1link.simulation.stock_rover import STOCK_ROVER_PROVIDER_PROFILE


def _unreal_profile() -> SimulationProviderProfile:
    return SimulationProviderProfile(
        profile_id="unreal-native-rover-v1",
        providers=(
            SimulationProviderDescriptor(
                provider_id="unreal-native",
                roles=(
                    ProviderRole.WORLD,
                    ProviderRole.PHYSICS,
                    ProviderRole.STATE,
                    ProviderRole.SENSOR,
                ),
                capabilities=(
                    "clock.simulation",
                    "state.vehicle-pose",
                    "truth.ground-truth",
                    "sensor.virtual",
                ),
            ),
            SimulationProviderDescriptor(
                provider_id="unreal-direct-control",
                roles=(ProviderRole.CONTROLLER,),
                capabilities=("command.rover-speed-steering/v1",),
            ),
        ),
        clock=SimulationClockDescriptor(
            provider_id="unreal-native",
            domain="unreal:fixed-step",
        ),
        control_profiles=(ControlProfile.ROVER_SPEED_STEERING_V1,),
    )


def test_stock_rover_provider_profile_round_trips_exactly() -> None:
    restored = SimulationProviderProfile.from_dict(STOCK_ROVER_PROVIDER_PROFILE.to_dict())

    assert restored == STOCK_ROVER_PROVIDER_PROFILE
    assert restored.clock.domain == "gazebo:/clock"
    assert any(ProviderRole.CONTROLLER in provider.roles for provider in restored.providers)


def test_provider_contract_accepts_unreal_without_changing_canonical_frames() -> None:
    profile = SimulationProviderProfile.from_dict(_unreal_profile().to_dict())

    assert profile.clock.domain == "unreal:fixed-step"
    assert profile.world_frame == "map_enu"
    assert profile.body_frame == "base_link_flu"


def test_provider_contract_rejects_missing_clock_capability() -> None:
    profile = _unreal_profile()
    world = profile.providers[0]

    with pytest.raises(SimulationProviderContractError, match="clock.simulation"):
        replace(
            profile,
            providers=(
                replace(
                    world,
                    capabilities=tuple(
                        capability
                        for capability in world.capabilities
                        if capability != "clock.simulation"
                    ),
                ),
                profile.providers[1],
            ),
        )


def test_provider_contract_rejects_undeclared_command_capability() -> None:
    profile = _unreal_profile()
    controller = profile.providers[1]

    with pytest.raises(SimulationProviderContractError, match="control profiles"):
        replace(
            profile,
            providers=(
                profile.providers[0],
                replace(controller, capabilities=("transport.custom",)),
            ),
        )
