"""Comprehensive round-trip tests for vidhi CLI integration.

Tests:
1. CLI argument parsing with nested configs
2. Default values work correctly
3. Nested polymorphic configs can be selected via CLI
4. dataclass_to_dict serialization works
5. Configurations are properly instantiated
6. YAML config file loading
7. Variant-specific field filtering for poly configs
8. User error scenarios (variant mismatch, invalid types, unknown args, etc.)
9. Schema definition errors (missing get_type, duplicate keys, etc.)
"""

import sys
from unittest.mock import patch

import pytest

from vidhi import (
    BasePolyConfig,
    dataclass_to_dict,
    field,
    frozen_dataclass,
    parse_cli_args,
)

# =============================================================================
# Test Data Classes
# =============================================================================


class SchedulerType:
    """Mock enum for scheduler type."""

    def __init__(self, name: str, value: str):
        self._name = name
        self._value = value

    @property
    def name(self):
        return self._name

    @property
    def value(self):
        return self._value


GREEDY = SchedulerType("GREEDY", "greedy")
BEAM = SchedulerType("BEAM", "beam")


@frozen_dataclass
class BaseSchedulerConfig(BasePolyConfig):
    """Base scheduler configuration."""

    @staticmethod
    def get_type():
        raise NotImplementedError


@frozen_dataclass
class GreedySchedulerConfig(BaseSchedulerConfig):
    """Greedy scheduler configuration."""

    temperature: float = field(default=1.0, help="Sampling temperature.")

    @staticmethod
    def get_type():
        return GREEDY


@frozen_dataclass
class BeamSchedulerConfig(BaseSchedulerConfig):
    """Beam search scheduler configuration."""

    beam_width: int = field(default=5, help="Beam search width.")
    length_penalty: float = field(default=1.0, help="Length penalty.")

    @staticmethod
    def get_type():
        return BEAM


@frozen_dataclass
class ModelConfig:
    """Model configuration."""

    name: str = field(default="gpt-4", help="Model name.")
    max_tokens: int = field(default=2048, help="Maximum tokens.")


@frozen_dataclass
class EngineConfig:
    """Engine configuration with polymorphic scheduler."""

    model_config: ModelConfig = field(
        default_factory=ModelConfig, help="Model configuration."
    )
    scheduler_config: BaseSchedulerConfig = field(
        default_factory=GreedySchedulerConfig, help="Scheduler configuration."
    )
    batch_size: int = field(default=32, help="Batch size.")


@frozen_dataclass
class ServerConfig:
    """Top-level server configuration."""

    host: str = field(default="localhost", help="Server host.")
    port: int = field(default=8000, help="Server port.")
    engine_config: EngineConfig = field(
        default_factory=EngineConfig, help="Engine configuration."
    )
    debug: bool = field(default=False, help="Enable debug mode.")

    @classmethod
    def create_from_cli_args(cls):
        return parse_cli_args(cls)


# =============================================================================
# Variant-specific field test data classes
# =============================================================================


class ControllerVariant:
    """Mock enum for controller variant type."""

    def __init__(self, name: str, value: str):
        self._name = name
        self._value = value

    @property
    def name(self):
        return self._name

    @property
    def value(self):
        return self._value


AUDIO_CTRL = ControllerVariant("AUDIO_CTRL", "audio_ctrl")
LLM_CTRL = ControllerVariant("LLM_CTRL", "llm_ctrl")
TTS_CTRL = ControllerVariant("TTS_CTRL", "tts_ctrl")


@frozen_dataclass
class SharedConfig:
    """Configuration shared by all variants."""

    batch_size: int = field(default=32, help="Batch size.")


@frozen_dataclass
class AbstractControllerConfig(BasePolyConfig):
    """Base controller configuration (outer poly).

    Shared fields go here; variant-specific fields in subclasses.
    """

    num_replicas: int = field(default=1, help="Number of replicas.")
    shared: SharedConfig = field(
        default_factory=SharedConfig, help="Shared configuration."
    )

    @staticmethod
    def get_type():
        raise NotImplementedError


@frozen_dataclass
class AudioControllerConfig(AbstractControllerConfig):
    """Audio controller - only has shared fields."""

    sample_rate: int = field(default=16000, help="Audio sample rate.")

    @staticmethod
    def get_type():
        return AUDIO_CTRL


@frozen_dataclass
class LlmControllerConfig(AbstractControllerConfig):
    """LLM controller - adds tokenizer workers and cache size."""

    num_tokenizer_workers: int = field(default=10, help="Number of tokenizer workers.")
    kv_cache_size: int = field(default=4096, help="KV cache size.")

    @staticmethod
    def get_type():
        return LLM_CTRL


@frozen_dataclass
class TtsControllerConfig(AbstractControllerConfig):
    """TTS controller - adds voice-specific fields."""

    max_audio_length: float = field(default=30.0, help="Max audio length in seconds.")

    @staticmethod
    def get_type():
        return TTS_CTRL


@frozen_dataclass
class VariantTestConfig:
    """Top-level config containing poly controller."""

    controller: AbstractControllerConfig = field(
        default_factory=LlmControllerConfig, help="Controller configuration."
    )
    log_level: str = field(default="info", help="Log level.")

    @classmethod
    def create_from_cli_args(cls):
        return parse_cli_args(cls)


# =============================================================================
# Helper for error tests
# =============================================================================


def _capture_exit(argv, config_cls):
    """Run parse_cli_args with given argv, capturing stderr on SystemExit.

    Returns (exit_code, stderr_text) if SystemExit is raised,
    raises AssertionError otherwise.
    """
    from io import StringIO

    stderr = StringIO()
    with patch.object(sys, "argv", argv):
        with patch("sys.stderr", stderr):
            try:
                if hasattr(config_cls, "create_from_cli_args"):
                    config_cls.create_from_cli_args()
                else:
                    parse_cli_args(config_cls)
            except SystemExit as e:
                return e.code, stderr.getvalue()
    raise AssertionError("Expected SystemExit but parsing succeeded")


# =============================================================================
# Tests: Default Values
# =============================================================================


class TestDefaultValues:
    """Test that default values are correctly applied."""

    def test_simple_defaults(self):
        """Test simple field defaults."""
        with patch.object(sys, "argv", ["test"]):
            config = ServerConfig.create_from_cli_args()

        assert config.host == "localhost"
        assert config.port == 8000
        assert config.debug is False

    def test_nested_defaults(self):
        """Test nested config defaults."""
        with patch.object(sys, "argv", ["test"]):
            config = ServerConfig.create_from_cli_args()

        assert config.engine_config.batch_size == 32
        assert config.engine_config.model_config.name == "gpt-4"
        assert config.engine_config.model_config.max_tokens == 2048

    def test_polymorphic_defaults(self):
        """Test polymorphic field defaults to GreedySchedulerConfig."""
        with patch.object(sys, "argv", ["test"]):
            config = ServerConfig.create_from_cli_args()

        assert isinstance(config.engine_config.scheduler_config, GreedySchedulerConfig)
        assert config.engine_config.scheduler_config.temperature == 1.0


# =============================================================================
# Tests: CLI Argument Parsing
# =============================================================================


class TestCLIArgumentParsing:
    """Test CLI argument parsing."""

    def test_simple_arg_parsing(self):
        """Test parsing simple arguments."""
        with patch.object(sys, "argv", ["test", "--host", "0.0.0.0", "--port", "9000"]):
            config = ServerConfig.create_from_cli_args()

        assert config.host == "0.0.0.0"
        assert config.port == 9000

    def test_nested_arg_parsing(self):
        """Test parsing nested config arguments."""
        with patch.object(
            sys,
            "argv",
            [
                "test",
                "--engine_config.batch_size",
                "64",
                "--engine_config.model_config.name",
                "llama-3",
                "--engine_config.model_config.max_tokens",
                "4096",
            ],
        ):
            config = ServerConfig.create_from_cli_args()

        assert config.engine_config.batch_size == 64
        assert config.engine_config.model_config.name == "llama-3"
        assert config.engine_config.model_config.max_tokens == 4096

    def test_boolean_flag_parsing(self):
        """Test parsing boolean flags."""
        with patch.object(sys, "argv", ["test", "--debug", "true"]):
            config = ServerConfig.create_from_cli_args()

        assert config.debug is True

    def test_combined_simple_and_nested(self):
        """Test parsing both simple and nested arguments."""
        with patch.object(
            sys,
            "argv",
            [
                "test",
                "--host",
                "0.0.0.0",
                "--port",
                "9000",
                "--engine_config.batch_size",
                "128",
                "--debug",
                "true",
            ],
        ):
            config = ServerConfig.create_from_cli_args()

        assert config.host == "0.0.0.0"
        assert config.port == 9000
        assert config.engine_config.batch_size == 128
        assert config.debug is True


# =============================================================================
# Tests: Polymorphic Config Selection
# =============================================================================


class TestPolymorphicConfigSelection:
    """Test polymorphic configuration selection via CLI."""

    def test_default_polymorphic_type(self):
        """Test default polymorphic type is greedy."""
        with patch.object(sys, "argv", ["test"]):
            config = ServerConfig.create_from_cli_args()

        assert isinstance(config.engine_config.scheduler_config, GreedySchedulerConfig)

    def test_select_beam_scheduler(self):
        """Test selecting beam scheduler via CLI."""
        with patch.object(
            sys,
            "argv",
            [
                "test",
                "--engine_config.scheduler_config.type",
                "beam",
            ],
        ):
            config = ServerConfig.create_from_cli_args()

        assert isinstance(config.engine_config.scheduler_config, BeamSchedulerConfig)
        assert config.engine_config.scheduler_config.beam_width == 5
        assert config.engine_config.scheduler_config.length_penalty == 1.0

    def test_select_beam_with_custom_params(self):
        """Test selecting beam scheduler with custom parameters."""
        with patch.object(
            sys,
            "argv",
            [
                "test",
                "--engine_config.scheduler_config.type",
                "beam",
                "--engine_config.scheduler_config.beam_width",
                "10",
                "--engine_config.scheduler_config.length_penalty",
                "0.5",
            ],
        ):
            config = ServerConfig.create_from_cli_args()

        assert isinstance(config.engine_config.scheduler_config, BeamSchedulerConfig)
        assert config.engine_config.scheduler_config.beam_width == 10
        assert config.engine_config.scheduler_config.length_penalty == 0.5

    def test_greedy_scheduler_with_custom_temp(self):
        """Test greedy scheduler with custom temperature."""
        with patch.object(
            sys,
            "argv",
            [
                "test",
                "--engine_config.scheduler_config.type",
                "greedy",
                "--engine_config.scheduler_config.temperature",
                "0.7",
            ],
        ):
            config = ServerConfig.create_from_cli_args()

        assert isinstance(config.engine_config.scheduler_config, GreedySchedulerConfig)
        assert config.engine_config.scheduler_config.temperature == 0.7


# =============================================================================
# Tests: Dataclass to Dict Serialization
# =============================================================================


class TestDataclassToDict:
    """Test dataclass_to_dict serialization."""

    def test_simple_serialization(self):
        """Test simple config serialization."""
        with patch.object(sys, "argv", ["test"]):
            config = ServerConfig.create_from_cli_args()

        d = dataclass_to_dict(config)

        assert d["host"] == "localhost"
        assert d["port"] == 8000
        assert d["debug"] is False

    def test_nested_serialization(self):
        """Test nested config serialization."""
        with patch.object(sys, "argv", ["test"]):
            config = ServerConfig.create_from_cli_args()

        d = dataclass_to_dict(config)

        assert "engine_config" in d
        assert d["engine_config"]["batch_size"] == 32
        assert d["engine_config"]["model_config"]["name"] == "gpt-4"
        assert d["engine_config"]["model_config"]["max_tokens"] == 2048

    def test_polymorphic_serialization(self):
        """Test polymorphic config serialization includes type."""
        with patch.object(
            sys,
            "argv",
            ["test", "--engine_config.scheduler_config.type", "beam"],
        ):
            config = ServerConfig.create_from_cli_args()

        d = dataclass_to_dict(config)

        scheduler_d = d["engine_config"]["scheduler_config"]
        # The type should be included
        assert "beam_width" in scheduler_d
        assert scheduler_d["beam_width"] == 5

    def test_roundtrip_consistency(self):
        """Test that serialization produces consistent results."""
        with patch.object(
            sys,
            "argv",
            [
                "test",
                "--host",
                "0.0.0.0",
                "--port",
                "9000",
                "--engine_config.batch_size",
                "64",
            ],
        ):
            config = ServerConfig.create_from_cli_args()

        d = dataclass_to_dict(config)

        # Verify all values are preserved
        assert d["host"] == "0.0.0.0"
        assert d["port"] == 9000
        assert d["engine_config"]["batch_size"] == 64


# =============================================================================
# Tests: YAML Config with CLI Overrides
# =============================================================================


class TestYAMLConfigWithCLIOverrides:
    """Test YAML config loading with CLI overrides."""

    def test_yaml_config_loading(self, tmp_path):
        """Test loading configuration from YAML file."""
        import yaml

        config_file = tmp_path / "config.yaml"
        config_data = {
            "host": "192.168.1.1",
            "port": 9090,
            "engine_config": {
                "batch_size": 64,
                "model_config": {
                    "name": "llama-3",
                    "max_tokens": 4096,
                },
            },
        }
        with open(config_file, "w") as f:
            yaml.dump(config_data, f)

        with patch.object(sys, "argv", ["test", "--config", str(config_file)]):
            config = ServerConfig.create_from_cli_args()

        assert config.host == "192.168.1.1"
        assert config.port == 9090
        assert config.engine_config.batch_size == 64
        assert config.engine_config.model_config.name == "llama-3"
        assert config.engine_config.model_config.max_tokens == 4096

    def test_yaml_config_with_cli_overrides(self, tmp_path):
        """Test that CLI arguments override YAML config values."""
        import yaml

        config_file = tmp_path / "config.yaml"
        config_data = {
            "host": "192.168.1.1",
            "port": 9090,
            "engine_config": {
                "batch_size": 64,
            },
        }
        with open(config_file, "w") as f:
            yaml.dump(config_data, f)

        with patch.object(
            sys,
            "argv",
            [
                "test",
                "--config",
                str(config_file),
                "--host",
                "0.0.0.0",
                "--engine_config.batch_size",
                "128",
            ],
        ):
            config = ServerConfig.create_from_cli_args()

        # CLI overrides
        assert config.host == "0.0.0.0"
        assert config.engine_config.batch_size == 128
        # YAML values (not overridden)
        assert config.port == 9090

    def test_yaml_polymorphic_config(self, tmp_path):
        """Test YAML config with polymorphic type selection."""
        import yaml

        config_file = tmp_path / "config.yaml"
        config_data = {
            "engine_config": {
                "scheduler_config": {
                    "type": "beam",
                    "beam_width": 10,
                    "length_penalty": 0.8,
                },
            },
        }
        with open(config_file, "w") as f:
            yaml.dump(config_data, f)

        with patch.object(sys, "argv", ["test", "--config", str(config_file)]):
            config = ServerConfig.create_from_cli_args()

        assert isinstance(config.engine_config.scheduler_config, BeamSchedulerConfig)
        assert config.engine_config.scheduler_config.beam_width == 10
        assert config.engine_config.scheduler_config.length_penalty == 0.8

    def test_yaml_polymorphic_with_cli_override(self, tmp_path):
        """Test YAML polymorphic config with CLI overrides."""
        import yaml

        config_file = tmp_path / "config.yaml"
        config_data = {
            "engine_config": {
                "scheduler_config": {
                    "type": "beam",
                    "beam_width": 10,
                },
            },
        }
        with open(config_file, "w") as f:
            yaml.dump(config_data, f)

        with patch.object(
            sys,
            "argv",
            [
                "test",
                "--config",
                str(config_file),
                "--engine_config.scheduler_config.beam_width",
                "20",
            ],
        ):
            config = ServerConfig.create_from_cli_args()

        assert isinstance(config.engine_config.scheduler_config, BeamSchedulerConfig)
        assert config.engine_config.scheduler_config.beam_width == 20

    def test_roundtrip_simple_config(self, tmp_path):
        """Test roundtrip with simple ServerConfig that has serializable values."""
        import yaml

        config_file = tmp_path / "config1.yaml"
        config_data = {
            "host": "10.0.0.1",
            "port": 8888,
            "debug": True,
            "engine_config": {
                "batch_size": 64,
                "model_config": {
                    "name": "test-model",
                    "max_tokens": 2048,
                },
            },
        }
        with open(config_file, "w") as f:
            yaml.dump(config_data, f)

        with patch.object(sys, "argv", ["test", "--config", str(config_file)]):
            config1 = ServerConfig.create_from_cli_args()

        assert config1.host == "10.0.0.1"
        assert config1.port == 8888
        assert config1.debug is True
        assert config1.engine_config.batch_size == 64
        assert config1.engine_config.model_config.name == "test-model"

        config_dict = dataclass_to_dict(config1)

        config_file2 = tmp_path / "config2.yaml"
        with open(config_file2, "w") as f:
            yaml.dump(config_dict, f)

        with patch.object(sys, "argv", ["test", "--config", str(config_file2)]):
            config2 = ServerConfig.create_from_cli_args()

        assert config2.host == config1.host
        assert config2.port == config1.port
        assert config2.debug == config1.debug
        assert config2.engine_config.batch_size == config1.engine_config.batch_size
        assert (
            config2.engine_config.model_config.name
            == config1.engine_config.model_config.name
        )


# =============================================================================
# Tests: Variant-Specific Nested Field Filtering
# =============================================================================


class TestVariantSpecificNestedFields:
    """Test that variant-specific fields are correctly filtered.

    When an outer poly config has variants with different sets of fields,
    each variant should only expose its own fields during CLI parsing
    and reconstruction.
    """

    def test_default_variant_reconstruction(self):
        """Test default variant (LLM) is constructed correctly."""
        with patch.object(sys, "argv", ["test"]):
            config = VariantTestConfig.create_from_cli_args()

        assert isinstance(config.controller, LlmControllerConfig)
        assert config.controller.num_tokenizer_workers == 10
        assert config.controller.kv_cache_size == 4096
        assert config.controller.num_replicas == 1

    def test_audio_variant_reconstruction(self):
        """Test audio variant has only shared + audio-specific fields."""
        with patch.object(sys, "argv", ["test", "--controller.type", "audio_ctrl"]):
            config = VariantTestConfig.create_from_cli_args()

        assert isinstance(config.controller, AudioControllerConfig)
        assert config.controller.sample_rate == 16000
        assert not hasattr(config.controller, "num_tokenizer_workers")
        assert not hasattr(config.controller, "kv_cache_size")

    def test_tts_variant_reconstruction(self):
        """Test TTS variant has only shared + tts-specific fields."""
        with patch.object(sys, "argv", ["test", "--controller.type", "tts_ctrl"]):
            config = VariantTestConfig.create_from_cli_args()

        assert isinstance(config.controller, TtsControllerConfig)
        assert config.controller.max_audio_length == 30.0
        assert not hasattr(config.controller, "num_tokenizer_workers")
        assert not hasattr(config.controller, "sample_rate")

    def test_llm_variant_with_custom_params(self):
        """Test LLM variant with custom nested parameters."""
        with patch.object(
            sys,
            "argv",
            [
                "test",
                "--controller.type",
                "llm_ctrl",
                "--controller.num_tokenizer_workers",
                "20",
                "--controller.kv_cache_size",
                "8192",
            ],
        ):
            config = VariantTestConfig.create_from_cli_args()

        assert isinstance(config.controller, LlmControllerConfig)
        assert config.controller.num_tokenizer_workers == 20
        assert config.controller.kv_cache_size == 8192

    def test_audio_variant_with_custom_sample_rate(self):
        """Test audio variant with custom sample rate."""
        with patch.object(
            sys,
            "argv",
            [
                "test",
                "--controller.type",
                "audio_ctrl",
                "--controller.sample_rate",
                "44100",
            ],
        ):
            config = VariantTestConfig.create_from_cli_args()

        assert isinstance(config.controller, AudioControllerConfig)
        assert config.controller.sample_rate == 44100

    def test_shared_nested_config_across_variants(self):
        """Test shared nested config is available in all variants."""
        with patch.object(
            sys,
            "argv",
            ["test", "--controller.shared.batch_size", "64"],
        ):
            config = VariantTestConfig.create_from_cli_args()
        assert config.controller.shared.batch_size == 64

        with patch.object(
            sys,
            "argv",
            [
                "test",
                "--controller.type",
                "audio_ctrl",
                "--controller.shared.batch_size",
                "128",
            ],
        ):
            config = VariantTestConfig.create_from_cli_args()
        assert config.controller.shared.batch_size == 128

    def test_switching_variant_discards_other_fields(self):
        """Test switching from LLM to audio doesn't carry over LLM fields."""
        with patch.object(sys, "argv", ["test", "--controller.type", "audio_ctrl"]):
            config = VariantTestConfig.create_from_cli_args()

        d = dataclass_to_dict(config)
        assert "sample_rate" in d["controller"]
        assert "num_tokenizer_workers" not in d["controller"]
        assert "kv_cache_size" not in d["controller"]

    def test_variant_only_field_not_in_other_variants(self):
        """Test variant-specific fields don't leak to other variants."""
        with patch.object(sys, "argv", ["test"]):
            config = VariantTestConfig.create_from_cli_args()
        assert hasattr(config.controller, "num_tokenizer_workers")

        with patch.object(sys, "argv", ["test", "--controller.type", "audio_ctrl"]):
            config = VariantTestConfig.create_from_cli_args()
        assert not hasattr(config.controller, "num_tokenizer_workers")


# =============================================================================
# Tests: Poly Field Variants Metadata
# =============================================================================


class TestPolyFieldVariants:
    """Test that poly_field_variants is correctly populated.

    Directly tests the walker metadata to verify variant filtering.
    """

    @pytest.fixture()
    def walker(self):
        from vidhi.nested_config import ConfigWalker

        walker = ConfigWalker(VariantTestConfig)
        walker.build_parser()
        return walker

    def test_num_tokenizer_workers_only_in_llm(self, walker):
        """num_tokenizer_workers should only list LLM as its variant."""
        pfv = walker.poly_field_variants

        tokenizer_key = "controller.num_tokenizer_workers"
        assert tokenizer_key in pfv

    def test_kv_cache_size_only_in_llm(self, walker):
        """kv_cache_size should only list LLM as its variant."""
        pfv = walker.poly_field_variants

        cache_key = "controller.kv_cache_size"
        assert cache_key in pfv

    def test_sample_rate_only_in_audio(self, walker):
        """sample_rate should only list audio as its variant."""
        pfv = walker.poly_field_variants

        rate_key = "controller.sample_rate"
        assert rate_key in pfv

    def test_max_audio_length_only_in_tts(self, walker):
        """max_audio_length should only list TTS as its variant."""
        pfv = walker.poly_field_variants

        length_key = "controller.max_audio_length"
        assert length_key in pfv

    def test_num_replicas_in_all_variants(self, walker):
        """num_replicas should be in all outer variants."""
        pfv = walker.poly_field_variants

        replicas_key = "controller.num_replicas"
        assert replicas_key in pfv

    def test_shared_batch_size_in_all_variants(self, walker):
        """shared.batch_size should be in all outer variants."""
        pfv = walker.poly_field_variants

        batch_key = "controller.shared.batch_size"
        assert batch_key in pfv


# =============================================================================
# Tests: Variant Serialization Roundtrip
# =============================================================================


class TestVariantSerializationRoundtrip:
    """Test serialization roundtrip with different variants."""

    def test_llm_variant_roundtrip(self, tmp_path):
        """Test LLM variant YAML roundtrip."""
        import yaml

        config_file = tmp_path / "config.yaml"
        config_data = {
            "controller": {
                "type": "llm_ctrl",
                "num_tokenizer_workers": 20,
                "kv_cache_size": 8192,
            },
        }
        with open(config_file, "w") as f:
            yaml.dump(config_data, f)

        with patch.object(sys, "argv", ["test", "--config", str(config_file)]):
            config = VariantTestConfig.create_from_cli_args()

        assert isinstance(config.controller, LlmControllerConfig)
        assert config.controller.num_tokenizer_workers == 20
        assert config.controller.kv_cache_size == 8192

    def test_audio_variant_roundtrip(self, tmp_path):
        """Test audio variant YAML roundtrip."""
        import yaml

        config_file = tmp_path / "config.yaml"
        config_data = {
            "controller": {
                "type": "audio_ctrl",
                "num_replicas": 2,
                "sample_rate": 44100,
            },
        }
        with open(config_file, "w") as f:
            yaml.dump(config_data, f)

        with patch.object(sys, "argv", ["test", "--config", str(config_file)]):
            config = VariantTestConfig.create_from_cli_args()

        assert isinstance(config.controller, AudioControllerConfig)
        assert config.controller.num_replicas == 2
        assert config.controller.sample_rate == 44100

    def test_tts_variant_roundtrip(self, tmp_path):
        """Test TTS variant YAML roundtrip."""
        import yaml

        config_file = tmp_path / "config.yaml"
        config_data = {
            "controller": {
                "type": "tts_ctrl",
                "max_audio_length": 60.0,
            },
        }
        with open(config_file, "w") as f:
            yaml.dump(config_data, f)

        with patch.object(sys, "argv", ["test", "--config", str(config_file)]):
            config = VariantTestConfig.create_from_cli_args()

        assert isinstance(config.controller, TtsControllerConfig)
        assert config.controller.max_audio_length == 60.0

    def test_variant_switch_via_cli_override(self, tmp_path):
        """Test switching variant type via CLI when YAML has another."""
        import yaml

        config_file = tmp_path / "config.yaml"
        config_data = {
            "controller": {
                "type": "llm_ctrl",
                "num_tokenizer_workers": 20,
            },
        }
        with open(config_file, "w") as f:
            yaml.dump(config_data, f)

        with patch.object(
            sys,
            "argv",
            [
                "test",
                "--config",
                str(config_file),
                "--controller.type",
                "audio_ctrl",
            ],
        ):
            config = VariantTestConfig.create_from_cli_args()

        assert isinstance(config.controller, AudioControllerConfig)


# =============================================================================
# Tests: User Error - Variant Mismatch
# =============================================================================


class TestUserErrorVariantMismatch:
    """Test errors when user passes fields from wrong polymorphic variant.

    This is the most common user error: selecting one variant (e.g., LLM)
    but passing configuration that belongs to a different variant (e.g., audio).
    """

    def test_llm_selected_audio_field_rejected(self):
        """Selecting LLM but passing audio's sample_rate should error."""
        code, stderr = _capture_exit(
            [
                "test",
                "--controller.type",
                "llm_ctrl",
                "--controller.sample_rate",
                "44100",
            ],
            VariantTestConfig,
        )
        assert code == 1
        assert "sample_rate" in stderr
        assert "audio_ctrl" in stderr
        assert "llm_ctrl" in stderr

    def test_audio_selected_llm_field_rejected(self):
        """Selecting audio but passing LLM's kv_cache_size should error."""
        code, stderr = _capture_exit(
            [
                "test",
                "--controller.type",
                "audio_ctrl",
                "--controller.kv_cache_size",
                "8192",
            ],
            VariantTestConfig,
        )
        assert code == 1
        assert "kv_cache_size" in stderr
        assert "llm_ctrl" in stderr
        assert "audio_ctrl" in stderr

    def test_audio_selected_llm_tokenizer_workers_rejected(self):
        """Selecting audio but passing LLM's num_tokenizer_workers should error."""
        code, stderr = _capture_exit(
            [
                "test",
                "--controller.type",
                "audio_ctrl",
                "--controller.num_tokenizer_workers",
                "20",
            ],
            VariantTestConfig,
        )
        assert code == 1
        assert "num_tokenizer_workers" in stderr
        assert "llm_ctrl" in stderr

    def test_tts_selected_audio_field_rejected(self):
        """Selecting TTS but passing audio's sample_rate should error."""
        code, stderr = _capture_exit(
            [
                "test",
                "--controller.type",
                "tts_ctrl",
                "--controller.sample_rate",
                "44100",
            ],
            VariantTestConfig,
        )
        assert code == 1
        assert "sample_rate" in stderr
        assert "audio_ctrl" in stderr
        assert "tts_ctrl" in stderr

    def test_tts_selected_llm_field_rejected(self):
        """Selecting TTS but passing LLM's kv_cache_size should error."""
        code, stderr = _capture_exit(
            [
                "test",
                "--controller.type",
                "tts_ctrl",
                "--controller.kv_cache_size",
                "4096",
            ],
            VariantTestConfig,
        )
        assert code == 1
        assert "kv_cache_size" in stderr
        assert "llm_ctrl" in stderr

    def test_implicit_default_variant_rejects_wrong_field(self):
        """Without explicit --type, default (LLM) should still reject audio fields."""
        code, stderr = _capture_exit(
            ["test", "--controller.sample_rate", "44100"],
            VariantTestConfig,
        )
        assert code == 1
        assert "sample_rate" in stderr
        assert "audio_ctrl" in stderr
        assert "llm_ctrl" in stderr

    def test_error_message_mentions_valid_variants(self):
        """Error message should tell the user which variant the field belongs to."""
        code, stderr = _capture_exit(
            [
                "test",
                "--controller.type",
                "tts_ctrl",
                "--controller.sample_rate",
                "44100",
            ],
            VariantTestConfig,
        )
        assert code == 1
        assert (
            "audio_ctrl" in stderr
        ), f"Error should mention 'audio_ctrl' as the valid variant. Got: {stderr}"


# =============================================================================
# Tests: User Error - Invalid Variant Type
# =============================================================================


class TestUserErrorInvalidVariantType:
    """Test errors when user specifies a variant type that doesn't exist."""

    def test_nonexistent_variant_type_rejected(self):
        """Passing a nonexistent variant type should error with valid choices."""
        code, stderr = _capture_exit(
            ["test", "--controller.type", "nonexistent"],
            VariantTestConfig,
        )
        assert code == 1
        assert "nonexistent" in stderr
        assert "audio_ctrl" in stderr
        assert "llm_ctrl" in stderr
        assert "tts_ctrl" in stderr

    def test_empty_variant_type_rejected(self):
        """Passing empty string for variant type should error."""
        code, _stderr = _capture_exit(
            ["test", "--controller.type", ""],
            VariantTestConfig,
        )
        assert code == 1

    def test_scheduler_invalid_type_rejected(self):
        """Passing invalid scheduler type should error with valid choices."""
        code, stderr = _capture_exit(
            ["test", "--engine_config.scheduler_config.type", "invalid_sched"],
            ServerConfig,
        )
        assert code == 1
        assert "invalid_sched" in stderr
        assert "greedy" in stderr
        assert "beam" in stderr

    def test_error_lists_all_valid_choices(self):
        """Error message should list all valid variant type choices."""
        code, stderr = _capture_exit(
            ["test", "--controller.type", "xyz"],
            VariantTestConfig,
        )
        assert code == 1
        assert "Choose from" in stderr or "audio_ctrl" in stderr


# =============================================================================
# Tests: User Error - Unknown Argument
# =============================================================================


class TestUserErrorUnknownArgument:
    """Test errors when user passes a completely unknown CLI argument."""

    def test_unknown_top_level_argument(self):
        """Passing an unknown top-level argument should error."""
        code, stderr = _capture_exit(
            ["test", "--nonexistent_field", "value"],
            VariantTestConfig,
        )
        assert code == 1
        assert "Unknown argument" in stderr
        assert "nonexistent_field" in stderr

    def test_unknown_nested_argument(self):
        """Passing an unknown nested argument should error."""
        code, stderr = _capture_exit(
            ["test", "--controller.nonexistent_sub_field", "value"],
            VariantTestConfig,
        )
        assert code == 1
        assert "Unknown argument" in stderr

    def test_misspelled_argument(self):
        """Misspelled argument should be reported as unknown."""
        code, stderr = _capture_exit(
            ["test", "--controller.num_replicass", "2"],  # extra 's'
            VariantTestConfig,
        )
        assert code == 1
        assert "Unknown argument" in stderr
        assert "num_replicass" in stderr


# =============================================================================
# Tests: User Error - Invalid Value Types
# =============================================================================


class TestUserErrorInvalidValueTypes:
    """Test errors when user passes values of the wrong type."""

    def test_string_for_int_field(self):
        """Passing a string where int is expected should error."""
        code, stderr = _capture_exit(
            ["test", "--port", "not_a_number"],
            ServerConfig,
        )
        assert code == 1
        assert "Invalid value" in stderr
        assert "port" in stderr

    def test_float_for_int_field(self):
        """Passing a float where int is expected should error."""
        code, stderr = _capture_exit(
            ["test", "--port", "3.14"],
            ServerConfig,
        )
        assert code == 1
        assert "Invalid value" in stderr
        assert "port" in stderr

    def test_string_for_nested_int_field(self):
        """Passing a string for a nested int field should error."""
        code, stderr = _capture_exit(
            ["test", "--controller.num_replicas", "many"],
            VariantTestConfig,
        )
        assert code == 1
        assert "Invalid value" in stderr
        assert "num_replicas" in stderr

    def test_string_for_deeply_nested_int(self):
        """Passing a string for a deeply nested int should error."""
        code, stderr = _capture_exit(
            ["test", "--controller.shared.batch_size", "abc"],
            VariantTestConfig,
        )
        assert code == 1
        assert "Invalid value" in stderr
        assert "batch_size" in stderr

    def test_invalid_boolean_value(self):
        """Passing an invalid boolean value should error with guidance."""
        code, stderr = _capture_exit(
            ["test", "--debug", "maybe"],
            ServerConfig,
        )
        assert code == 1
        assert "Invalid boolean" in stderr
        assert "debug" in stderr
        assert "true/false" in stderr or "yes/no" in stderr or "1/0" in stderr

    def test_string_for_float_field(self):
        """Passing a non-numeric string for float field should error."""
        code, stderr = _capture_exit(
            ["test", "--engine_config.scheduler_config.temperature", "warm"],
            ServerConfig,
        )
        assert code == 1
        assert "Invalid value" in stderr
        assert "temperature" in stderr


# =============================================================================
# Tests: User Error - Missing Values
# =============================================================================


class TestUserErrorMissingValues:
    """Test errors when user forgets to provide a value for an argument."""

    def test_missing_value_at_end(self):
        """Argument at end of command line without value should error."""
        code, stderr = _capture_exit(
            ["test", "--port"],
            ServerConfig,
        )
        assert code == 1
        assert "requires a value" in stderr
        assert "port" in stderr

    def test_missing_value_before_next_flag(self):
        """Argument followed by another flag (no value) should error."""
        code, stderr = _capture_exit(
            ["test", "--port", "--debug", "true"],
            ServerConfig,
        )
        assert code == 1
        assert "port" in stderr

    def test_config_without_path(self):
        """--config without a file path should error."""
        code, stderr = _capture_exit(
            ["test", "--config"],
            ServerConfig,
        )
        assert code == 1
        assert "config" in stderr.lower()


# =============================================================================
# Tests: User Error - Config File
# =============================================================================


class TestUserErrorConfigFile:
    """Test errors related to config file loading."""

    def test_nonexistent_config_file(self):
        """Pointing to a nonexistent config file should error."""
        with pytest.raises(FileNotFoundError, match="not found"):
            with patch.object(
                sys, "argv", ["test", "--config", "/nonexistent/path/config.yaml"]
            ):
                ServerConfig.create_from_cli_args()

    def test_yaml_invalid_variant_type(self, tmp_path):
        """YAML with invalid variant type should raise ValueError."""
        import yaml

        config_file = tmp_path / "config.yaml"
        config_data = {
            "controller": {
                "type": "nonexistent_ctrl",
            },
        }
        with open(config_file, "w") as f:
            yaml.dump(config_data, f)

        with pytest.raises(ValueError, match="No subclass of.*matches type value"):
            with patch.object(sys, "argv", ["test", "--config", str(config_file)]):
                VariantTestConfig.create_from_cli_args()

    def test_yaml_invalid_variant_type_shows_valid_options(self, tmp_path):
        """YAML with invalid variant type should list valid options."""
        import yaml

        config_file = tmp_path / "config.yaml"
        config_data = {
            "controller": {
                "type": "invalid",
            },
        }
        with open(config_file, "w") as f:
            yaml.dump(config_data, f)

        with pytest.raises(
            ValueError, match="audio_ctrl.*llm_ctrl.*tts_ctrl|Valid types"
        ):
            with patch.object(sys, "argv", ["test", "--config", str(config_file)]):
                VariantTestConfig.create_from_cli_args()

    def test_yaml_invalid_scheduler_type(self, tmp_path):
        """YAML with invalid scheduler type should raise ValueError."""
        import yaml

        config_file = tmp_path / "config.yaml"
        config_data = {
            "engine_config": {
                "scheduler_config": {
                    "type": "random_search",
                },
            },
        }
        with open(config_file, "w") as f:
            yaml.dump(config_data, f)

        with pytest.raises(ValueError, match="No subclass of.*matches type value"):
            with patch.object(sys, "argv", ["test", "--config", str(config_file)]):
                ServerConfig.create_from_cli_args()


# =============================================================================
# Tests: User Error - Shared Fields Still Work (Positive Controls)
# =============================================================================


class TestUserErrorSharedFieldsStillWork:
    """Verify shared fields are accepted by all variants (positive control).

    These complement the error tests to ensure we don't over-reject.
    """

    def test_shared_num_replicas_accepted_by_all_variants(self):
        """num_replicas (base class field) should work for all variants."""
        for variant in ["llm_ctrl", "audio_ctrl", "tts_ctrl"]:
            with patch.object(
                sys,
                "argv",
                [
                    "test",
                    "--controller.type",
                    variant,
                    "--controller.num_replicas",
                    "4",
                ],
            ):
                config = VariantTestConfig.create_from_cli_args()
            assert config.controller.num_replicas == 4

    def test_shared_nested_batch_size_accepted_by_all_variants(self):
        """shared.batch_size should work for all variants."""
        for variant in ["llm_ctrl", "audio_ctrl", "tts_ctrl"]:
            with patch.object(
                sys,
                "argv",
                [
                    "test",
                    "--controller.type",
                    variant,
                    "--controller.shared.batch_size",
                    "128",
                ],
            ):
                config = VariantTestConfig.create_from_cli_args()
            assert config.controller.shared.batch_size == 128

    def test_variant_specific_field_accepted_by_correct_variant(self):
        """Each variant-specific field should be accepted by its own variant."""
        # LLM accepts kv_cache_size
        with patch.object(
            sys,
            "argv",
            [
                "test",
                "--controller.type",
                "llm_ctrl",
                "--controller.kv_cache_size",
                "8192",
            ],
        ):
            config = VariantTestConfig.create_from_cli_args()
        assert config.controller.kv_cache_size == 8192

        # Audio accepts sample_rate
        with patch.object(
            sys,
            "argv",
            [
                "test",
                "--controller.type",
                "audio_ctrl",
                "--controller.sample_rate",
                "44100",
            ],
        ):
            config = VariantTestConfig.create_from_cli_args()
        assert config.controller.sample_rate == 44100

        # TTS accepts max_audio_length
        with patch.object(
            sys,
            "argv",
            [
                "test",
                "--controller.type",
                "tts_ctrl",
                "--controller.max_audio_length",
                "60.0",
            ],
        ):
            config = VariantTestConfig.create_from_cli_args()
        assert config.controller.max_audio_length == 60.0


# =============================================================================
# Tests: Schema Definition Errors - Missing get_type()
# =============================================================================


class TestSchemaErrorMissingGetType:
    """Test errors when poly variants don't implement get_type()."""

    def test_variant_without_get_type_raises(self):
        """Variant that doesn't override get_type() should error at walker build."""
        from vidhi.nested_config import ConfigWalker

        @frozen_dataclass
        class _Base(BasePolyConfig):
            @staticmethod
            def get_type():
                raise NotImplementedError

        @frozen_dataclass
        class _Variant(_Base):
            x: int = field(default=1, help="x")
            # Missing get_type()

        @frozen_dataclass
        class _Top:
            sub: _Base = field(default_factory=_Variant, help="sub")

        with pytest.raises(ValueError, match="get_type.*not implemented"):
            walker = ConfigWalker(_Top)
            walker.build_parser()

    def test_error_names_the_offending_class(self):
        """Error message should name the class that's missing get_type()."""
        from vidhi.nested_config import ConfigWalker

        @frozen_dataclass
        class _Base2(BasePolyConfig):
            @staticmethod
            def get_type():
                raise NotImplementedError

        @frozen_dataclass
        class _MissingImpl(_Base2):
            val: int = field(default=0, help="val")

        @frozen_dataclass
        class _Top2:
            sub: _Base2 = field(default_factory=_MissingImpl, help="sub")

        with pytest.raises(ValueError, match="_MissingImpl"):
            walker = ConfigWalker(_Top2)
            walker.build_parser()

    def test_error_suggests_fix(self):
        """Error should suggest how to fix the missing get_type()."""
        from vidhi.nested_config import ConfigWalker

        @frozen_dataclass
        class _Base3(BasePolyConfig):
            @staticmethod
            def get_type():
                raise NotImplementedError

        @frozen_dataclass
        class _NoType(_Base3):
            val: int = field(default=0, help="val")

        @frozen_dataclass
        class _Top3:
            sub: _Base3 = field(default_factory=_NoType, help="sub")

        with pytest.raises(ValueError, match="get_type.*not implemented"):
            walker = ConfigWalker(_Top3)
            walker.build_parser()


# =============================================================================
# Tests: Schema Definition Errors - Duplicate Type Keys
# =============================================================================


class TestSchemaErrorDuplicateTypeKeys:
    """Test behavior when multiple variants share the same type key."""

    def test_duplicate_type_key_last_wins(self):
        """Two variants returning same type value — last registered wins."""
        from vidhi.nested_config import ConfigWalker

        shared_type = ControllerVariant("DUP", "dup")

        @frozen_dataclass
        class _DupBase(BasePolyConfig):
            @staticmethod
            def get_type():
                raise NotImplementedError

        @frozen_dataclass
        class _DupV1(_DupBase):
            x: int = field(default=1, help="x")

            @staticmethod
            def get_type():
                return shared_type

        @frozen_dataclass
        class _DupV2(_DupBase):
            y: int = field(default=2, help="y")

            @staticmethod
            def get_type():
                return shared_type  # Same type key!

        @frozen_dataclass
        class _DupTop:
            sub: _DupBase = field(default_factory=_DupV1, help="sub")

        # Walker accepts duplicate type keys (last registered wins)
        walker = ConfigWalker(_DupTop)
        walker.build_parser()
        assert "dup" in walker.poly_children["sub"]


# =============================================================================
# Tests: Schema Definition Errors - Type Conflict
# =============================================================================


class TestSchemaErrorTypeConflict:
    """Test that variants with same field name but different types are handled."""

    def test_same_field_different_types_uses_default_variant(self):
        """Same field name with different types — walker uses default variant's type."""
        from vidhi.nested_config import ConfigWalker

        type_a = ControllerVariant("VA", "va")
        type_b = ControllerVariant("VB", "vb")

        @frozen_dataclass
        class _ConflictBase(BasePolyConfig):
            @staticmethod
            def get_type():
                raise NotImplementedError

        @frozen_dataclass
        class _ConflictV1(_ConflictBase):
            temperature: float = field(default=1.0, help="temp")

            @staticmethod
            def get_type():
                return type_a

        @frozen_dataclass
        class _ConflictV2(_ConflictBase):
            temperature: str = field(default="warm", help="temp")

            @staticmethod
            def get_type():
                return type_b

        @frozen_dataclass
        class _ConflictTop:
            sub: _ConflictBase = field(default_factory=_ConflictV1, help="sub")

        # Walker registers the field using the default variant's type
        walker = ConfigWalker(_ConflictTop)
        parser = walker.build_parser()
        assert "sub.temperature" in parser.arguments


# =============================================================================
# Tests: Schema Definition Errors - Poly with No Subclasses
# =============================================================================


class TestSchemaErrorPolyNoSubclasses:
    """Test errors when poly config has no concrete subclasses."""

    def test_poly_base_with_no_subclasses_raises(self):
        """BasePolyConfig with no subclasses should error at walker build."""
        from vidhi.nested_config import ConfigWalker

        @frozen_dataclass
        class _Orphan(BasePolyConfig):
            val: int = field(default=1, help="val")

            @staticmethod
            def get_type():
                return ControllerVariant("ORPHAN", "orphan")

        @frozen_dataclass
        class _OrphanTop:
            sub: _Orphan = field(default_factory=_Orphan, help="sub")

        with pytest.raises(ValueError, match="no subclasses"):
            walker = ConfigWalker(_OrphanTop)
            walker.build_parser()

    def test_error_suggests_removing_basepoly(self):
        """Error should suggest removing BasePolyConfig if polymorphism isn't needed."""
        from vidhi.nested_config import ConfigWalker

        @frozen_dataclass
        class _UnnecessaryPoly(BasePolyConfig):
            val: int = field(default=1, help="val")

            @staticmethod
            def get_type():
                return ControllerVariant("SINGLE", "single")

        @frozen_dataclass
        class _UnnecessaryTop:
            sub: _UnnecessaryPoly = field(default_factory=_UnnecessaryPoly, help="sub")

        with pytest.raises(ValueError, match="no subclasses"):
            walker = ConfigWalker(_UnnecessaryTop)
            walker.build_parser()


# =============================================================================
# Tests: Schema Definition Errors - Mutable Default
# =============================================================================


class TestSchemaErrorMutableDefault:
    """Test errors when mutable defaults are used (caught by Python dataclasses)."""

    def test_list_default_raises(self):
        """Mutable list default should be caught at definition time."""
        with pytest.raises(ValueError, match="mutable default.*list"):

            @frozen_dataclass
            class _MutableList:
                items: list = field(default=[1, 2, 3], help="items")

    def test_dict_default_raises(self):
        """Mutable dict default should be caught at definition time."""
        with pytest.raises(ValueError, match="mutable default.*dict"):

            @frozen_dataclass
            class _MutableDict:
                data: dict = field(default={"a": 1}, help="data")

    def test_set_default_raises(self):
        """Mutable set default should be caught at definition time."""
        with pytest.raises(ValueError, match="mutable default.*set"):

            @frozen_dataclass
            class _MutableSet:
                tags: set = field(default={1, 2}, help="tags")


# =============================================================================
# Tests: Schema - Frozen Immutability
# =============================================================================


class TestSchemaFrozenImmutability:
    """Test that frozen dataclasses are truly immutable."""

    def test_frozen_field_cannot_be_mutated(self):
        """Assigning to a field on a frozen dataclass should raise."""

        @frozen_dataclass
        class _Frozen:
            name: str = field(default="test", help="name")

        obj = _Frozen()
        with pytest.raises(AttributeError, match="cannot assign"):
            obj.name = "changed"

    def test_nested_frozen_field_cannot_be_mutated(self):
        """Assigning to nested frozen dataclass field should raise."""

        @frozen_dataclass
        class _Inner:
            val: int = field(default=10, help="val")

        @frozen_dataclass
        class _Outer:
            inner: _Inner = field(default_factory=_Inner, help="inner")

        obj = _Outer()
        with pytest.raises(AttributeError, match="cannot assign"):
            obj.inner.val = 99


# =============================================================================
# Tests: Schema - Valid Correct Definitions (Positive Controls)
# =============================================================================


class TestSchemaValidCorrectDefinitions:
    """Positive tests: correctly defined schemas should work without error."""

    def test_same_dataclass_reused_in_multiple_fields(self):
        """Same frozen_dataclass type used in multiple fields should work."""
        from vidhi.nested_config import ConfigWalker

        @frozen_dataclass
        class _Shared:
            val: int = field(default=10, help="val")

        @frozen_dataclass
        class _MultiUse:
            primary: _Shared = field(default_factory=_Shared, help="primary")
            secondary: _Shared = field(default_factory=_Shared, help="secondary")

        walker = ConfigWalker(_MultiUse)
        parser = walker.build_parser()
        assert "primary.val" in parser.arguments
        assert "secondary.val" in parser.arguments

        with patch.object(
            sys,
            "argv",
            ["test", "--primary.val", "20", "--secondary.val", "30"],
        ):
            config = parse_cli_args(_MultiUse)
        assert config.primary.val == 20
        assert config.secondary.val == 30

    def test_poly_variant_with_nested_dataclass_field(self):
        """Poly variant that adds a nested frozen_dataclass should work."""
        from vidhi.nested_config import ConfigWalker

        type_rich = ControllerVariant("RICH", "rich")
        type_lite = ControllerVariant("LITE", "lite")

        @frozen_dataclass
        class _ExtraSub:
            depth: int = field(default=3, help="depth")

        @frozen_dataclass
        class _VarBase(BasePolyConfig):
            shared: int = field(default=0, help="shared")

            @staticmethod
            def get_type():
                raise NotImplementedError

        @frozen_dataclass
        class _RichVariant(_VarBase):
            extra: _ExtraSub = field(default_factory=_ExtraSub, help="extra")

            @staticmethod
            def get_type():
                return type_rich

        @frozen_dataclass
        class _LiteVariant(_VarBase):
            simple: str = field(default="yes", help="simple")

            @staticmethod
            def get_type():
                return type_lite

        @frozen_dataclass
        class _VarTop:
            sub: _VarBase = field(default_factory=_RichVariant, help="sub")

        walker = ConfigWalker(_VarTop)
        walker.build_parser()
        pfv = walker.poly_field_variants

        # extra.depth should only be in the rich variant
        assert "sub.extra.depth" in pfv
        # simple should only be in lite variant
        assert "sub.simple" in pfv
        # shared should be in both
        assert "sub.shared" in pfv

    def test_same_field_same_type_across_variants_is_valid(self):
        """Same field name with same type across variants should not error."""
        from vidhi.nested_config import ConfigWalker

        type_m = ControllerVariant("M", "m")
        type_n = ControllerVariant("N", "n")

        @frozen_dataclass
        class _CompatBase(BasePolyConfig):
            @staticmethod
            def get_type():
                raise NotImplementedError

        @frozen_dataclass
        class _CompatV1(_CompatBase):
            rate: float = field(default=1.0, help="rate")

            @staticmethod
            def get_type():
                return type_m

        @frozen_dataclass
        class _CompatV2(_CompatBase):
            rate: float = field(default=2.0, help="rate")

            @staticmethod
            def get_type():
                return type_n

        @frozen_dataclass
        class _CompatTop:
            sub: _CompatBase = field(default_factory=_CompatV1, help="sub")

        # Should NOT raise - same field name with same type is fine
        walker = ConfigWalker(_CompatTop)
        walker.build_parser()
        pfv = walker.poly_field_variants
        assert pfv["sub.rate"]["variants"] == {"m", "n"}

    def test_optional_poly_field_with_none_default(self):
        """Optional poly field defaulting to None should work."""
        from typing import Optional

        from vidhi.nested_config import ConfigWalker

        type_opt = ControllerVariant("OPT", "opt")

        @frozen_dataclass
        class _OptBase(BasePolyConfig):
            @staticmethod
            def get_type():
                raise NotImplementedError

        @frozen_dataclass
        class _OptVariant(_OptBase):
            flag: bool = field(default=True, help="flag")

            @staticmethod
            def get_type():
                return type_opt

        @frozen_dataclass
        class _OptTop:
            sub: Optional[_OptBase] = field(default=None, help="sub")
            name: str = field(default="test", help="name")

        walker = ConfigWalker(_OptTop)
        parser = walker.build_parser()
        assert "sub.type" in parser.arguments
        assert "name" in parser.arguments

    def test_dict_field_treated_as_primitive(self):
        """Dict field should be treated as a primitive (not walked into)."""
        from typing import Dict

        from vidhi.nested_config import ConfigWalker

        @frozen_dataclass
        class _DictConfig:
            metadata: Dict[str, str] = field(default_factory=dict, help="metadata")
            name: str = field(default="test", help="name")

        walker = ConfigWalker(_DictConfig)
        parser = walker.build_parser()
        arg_names = set(parser.arguments.keys())
        # Dict field should appear as-is (not walked into sub-fields)
        assert "metadata" in arg_names
        assert "name" in arg_names


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
