#!/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.
"""TrainPipelineConfig integration for the distributed fields: fail-fasts + config compat."""

from types import SimpleNamespace

import draccus
import pytest

from lerobot.configs.accelerator import ActivationCheckpointingMode
from lerobot.configs.default import DatasetConfig, PeftConfig
from lerobot.configs.parallelism import ContextParallelConfig, ParallelismConfig
from lerobot.configs.train import CheckpointFormat, TrainPipelineConfig
from lerobot.optim.optimizers import AdamConfig, MultiAdamConfig


def make_cfg(**overrides) -> TrainPipelineConfig:
    cfg = TrainPipelineConfig(dataset=DatasetConfig(repo_id="lerobot/dummy"))
    for name, value in overrides.items():
        setattr(cfg, name, value)
    return cfg


def sharded() -> ParallelismConfig:
    return ParallelismConfig(dp_shard=-1)


class TestDistributedFailFasts:
    def test_defaults_pass(self):
        make_cfg()._validate_distributed()

    def test_cp_reserved(self):
        cfg = make_cfg(parallelism=ParallelismConfig(context_parallel=ContextParallelConfig(ring_degree=2)))
        with pytest.raises(ValueError, match="not implemented"):
            cfg._validate_distributed()

    def test_cfg_parallel_training_rejected(self):
        cfg = make_cfg(parallelism=ParallelismConfig(cfg_parallel=2))
        with pytest.raises(ValueError, match="inference-only"):
            cfg._validate_distributed()

    def test_compile_placeholder(self):
        cfg = make_cfg()
        cfg.accelerator.compile.enabled = True
        with pytest.raises(ValueError, match="compile"):
            cfg._validate_distributed()

    def test_activation_checkpointing_placeholder(self):
        cfg = make_cfg()
        cfg.accelerator.activation_checkpointing.mode = ActivationCheckpointingMode.FULL
        with pytest.raises(ValueError, match="activation_checkpointing"):
            cfg._validate_distributed()

    def test_dcp_format_requires_sharding(self):
        cfg = make_cfg(checkpoint_format=CheckpointFormat.DCP)
        with pytest.raises(ValueError, match="sharded"):
            cfg._validate_distributed()
        cfg.parallelism = sharded()
        cfg._validate_distributed()

    @pytest.mark.parametrize("mixed_precision", ["no", "bf16", "fp16"])
    def test_every_precision_is_allowed_when_sharded(self, mixed_precision):
        """fp16 was rejected under sharding while the GradScaler-over-DTensor path was
        unverified; accelerate builds a plain GradScaler for FSDP2 and torch reduces the
        overflow flag across the mesh, so all three precisions are now in scope."""
        cfg = make_cfg(parallelism=sharded())
        cfg.accelerator.mixed_precision = mixed_precision
        cfg._validate_distributed()

    def test_fp16_rejected_on_cpu(self):
        """accelerate skips the whole fp16 branch on CPU, so the run would silently be fp32."""
        cfg = make_cfg()
        cfg.policy = SimpleNamespace(device="cpu")
        cfg.accelerator.mixed_precision = "fp16"
        with pytest.raises(ValueError, match="fp16 requires an accelerator device"):
            cfg._validate_distributed()

        cfg.accelerator.mixed_precision = "bf16"
        cfg._validate_distributed()
        cfg.accelerator.mixed_precision = "fp16"
        cfg.policy = SimpleNamespace(device="cuda")
        cfg._validate_distributed()

    def test_peft_rejected_when_sharded(self):
        cfg = make_cfg(parallelism=sharded(), peft=PeftConfig())
        with pytest.raises(ValueError, match="PEFT"):
            cfg._validate_distributed()

    def test_env_eval_rejected_when_sharded(self):
        cfg = make_cfg(parallelism=sharded(), env_eval_freq=1000)
        cfg.env = object()  # any configured env triggers the check
        with pytest.raises(ValueError, match="environment evaluation"):
            cfg._validate_distributed()

    def test_multi_optimizer_rejected_when_sharded(self):
        cfg = make_cfg(parallelism=sharded(), optimizer=MultiAdamConfig())
        with pytest.raises(ValueError, match="Multi-optimizer"):
            cfg._validate_distributed()
        cfg.optimizer = AdamConfig()
        cfg._validate_distributed()


class TestConfigCompat:
    def test_checkpoint_format_round_trip(self):
        for fmt in CheckpointFormat:
            assert draccus.decode(CheckpointFormat, draccus.encode(fmt)) is fmt

    def test_wants_predicates(self):
        assert CheckpointFormat.SAFETENSORS.wants_safetensors
        assert not CheckpointFormat.SAFETENSORS.wants_dcp
        assert CheckpointFormat.DCP.wants_dcp and not CheckpointFormat.DCP.wants_safetensors
        both = CheckpointFormat.SAFETENSORS_AND_DCP
        assert both.wants_safetensors and both.wants_dcp


def test_reward_model_rejected_when_sharded():
    """Sharded reward runs previously failed late (missing wrap
    units, DTensor serialization at the first checkpoint) instead of at validation."""
    cfg = make_cfg(parallelism=sharded())
    cfg.reward_model = object()  # any configured reward model triggers the check
    with pytest.raises(ValueError, match="Reward-model"):
        cfg._validate_distributed()
