#!/usr/bin/env python

# Copyright 2026 The HuggingFace Inc. team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Version canaries for the accelerate/torch seams LeRobot's distributed engine relies on.

LeRobot deliberately builds on a few accelerate internals that are not covered by a public
stability promise. These tests exist to fail LOUDLY on a
dependency upgrade — on a CPU runner, before any distributed job can be corrupted — whenever one
of those seams moves. If a canary fails, re-audit the corresponding integration seam before bumping
the pin; do not simply update the assertion.
"""

import inspect

import pytest

pytest.importorskip("accelerate", reason="accelerate is required (install lerobot[training])")


def test_fsdp_checkpoint_name_constants():
    """Checkpoint dir names are imported from accelerate; the on-disk layout depends on them."""
    from accelerate.utils.constants import FSDP_MODEL_NAME, OPTIMIZER_NAME

    assert FSDP_MODEL_NAME == "pytorch_model_fsdp"
    assert OPTIMIZER_NAME == "optimizer"


def test_parallelism_config_mesh_dim_contract():
    """FSDP2 shards over the flattened dp_shard_cp dim; the dataloader keys on exact root names."""
    from accelerate.parallelism_config import ParallelismConfig

    pc = ParallelismConfig(dp_replicate_size=2, dp_shard_size=2, cp_size=2)
    assert pc.fsdp_dim_names == ["dp_replicate", "dp_shard_cp"]
    assert pc.dp_shard_cp_dim_names == ["dp_shard", "cp"]
    assert pc.dp_cp_dim_names == ["dp_replicate", "dp_shard", "cp"]
    # Degenerate FSDP-only case still shards over the flattened name.
    pc_fsdp = ParallelismConfig(dp_replicate_size=1, dp_shard_size=4)
    assert pc_fsdp.fsdp_dim_names == ["dp_shard_cp"]


def test_accelerator_accepts_parallelism_config():
    from accelerate import Accelerator

    params = inspect.signature(Accelerator.__init__).parameters
    assert "parallelism_config" in params
    assert "fsdp_plugin" in params
    assert "gradient_accumulation_plugin" in params


def test_dataloader_is_mesh_aware():
    """prepare_data_loader must accept the device mesh that makes CP peers share batches."""
    from accelerate.data_loader import prepare_data_loader

    assert "torch_device_mesh" in inspect.signature(prepare_data_loader).parameters


def test_cp_mask_stripping_hook_seam():
    """finalize_sharded_policy strips this exact hook.

    If accelerate renames or moves it, the strip becomes a silent no-op and CP training would
    inherit mask-corrupting hooks — hence a canary rather than a runtime hasattr.
    """
    from accelerate.big_modeling import _attach_context_parallel_hooks

    assert callable(_attach_context_parallel_hooks)
    assert _attach_context_parallel_hooks.__module__ == "accelerate.big_modeling"


def test_fsdp_plugin_mirrored_fields_exist():
    """AcceleratorConfig mirrors a plain-typed subset of the plugin; the fields must survive."""
    from accelerate.utils import FullyShardedDataParallelPlugin

    fields = {f.name for f in FullyShardedDataParallelPlugin.__dataclass_fields__.values()}
    assert {
        "fsdp_version",
        "reshard_after_forward",
        "auto_wrap_policy",
        "transformer_cls_names_to_wrap",
        "min_num_params",
        "cpu_offload",
        "ignored_modules",
        "activation_checkpointing",
        "state_dict_type",
    } <= fields


def test_merge_fsdp_weights_signature():
    """The DCP->safetensors converter is a thin wrapper over this accelerate utility."""
    from accelerate.utils import merge_fsdp_weights

    params = inspect.signature(merge_fsdp_weights).parameters
    assert {"checkpoint_dir", "output_path", "safe_serialization"} <= set(params)


def test_fsdp_save_load_helpers_exist():
    from accelerate.utils import (
        load_fsdp_model,
        load_fsdp_optimizer,
        save_fsdp_model,
        save_fsdp_optimizer,
    )

    for fn in (save_fsdp_model, load_fsdp_model, save_fsdp_optimizer, load_fsdp_optimizer):
        assert callable(fn)


def test_torch_fsdp2_seams():
    """isinstance(FSDPModule) discrimination + non-forward entry registration + full gather."""
    from torch.distributed.checkpoint.state_dict import (
        StateDictOptions,
        get_model_state_dict,  # noqa: F401
    )
    from torch.distributed.fsdp import FSDPModule, register_fsdp_forward_method  # noqa: F401

    options = inspect.signature(StateDictOptions).parameters
    assert {"full_state_dict", "cpu_offload"} <= set(options)


def test_accelerate_version_floor():
    import accelerate
    from packaging import version

    if version.parse(accelerate.__version__) < version.parse("1.14.0"):
        pytest.fail(
            f"accelerate {accelerate.__version__} < 1.14.0: the FSDP2 auto-wrap fallback fix "
            "(#3999) and the bf16->fp32 master-weight upcast this design relies on are absent."
        )
