"""Tests for the model detection utility."""

import json
from unittest.mock import patch

from mlx_manager.utils.model_detection import (
    ARCHITECTURE_FAMILIES,
    MODEL_FAMILY_MIN_VERSIONS,
    check_mlx_lm_support,
    detect_model_family,
    detect_multimodal,
    extract_characteristics,
    extract_characteristics_from_model,
    get_local_model_path,
    get_mlx_lm_version,
    get_model_detection_info,
    get_parser_options,
    normalize_architecture,
    parse_version,
    read_model_config,
)


class TestFuzzyParserOptions:
    """Tests for parser options matching.

    NOTE: Parser options are no longer used with the embedded MLX Server.
    All tests verify that get_parser_options returns empty dict.
    """

    def test_all_models_return_empty(self):
        """Parser options always return empty dict (feature deprecated)."""
        assert get_parser_options("mlx-community/MiniMax-M2.1-3bit") == {}
        assert get_parser_options("mlx-community/Qwen3-8B-4bit") == {}
        assert get_parser_options("mlx-community/Qwen3-Coder-7B-4bit") == {}
        assert get_parser_options("mlx-community/GLM-4-9B") == {}
        assert get_parser_options("mlx-community/GLM-4-MoE") == {}
        assert get_parser_options("mlx-community/Nemotron-3-8B") == {}
        assert get_parser_options("mlx-community/Llama-3.1-70B-4bit") == {}


class TestGetLocalModelPath:
    """Tests for get_local_model_path function."""

    def test_returns_none_when_model_not_downloaded(self, tmp_path):
        """Test returns None when model is not in cache."""
        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            result = get_local_model_path("mlx-community/nonexistent-model")
            assert result is None

    def test_returns_snapshot_path_when_downloaded(self, tmp_path):
        """Test returns snapshot path when model is downloaded."""
        # Create mock cache structure
        model_dir = tmp_path / "models--mlx-community--test-model" / "snapshots"
        snapshot_dir = model_dir / "abc123"
        snapshot_dir.mkdir(parents=True)

        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            result = get_local_model_path("mlx-community/test-model")
            assert result == snapshot_dir

    def test_returns_most_recent_snapshot(self, tmp_path):
        """Test returns most recent snapshot when multiple exist."""
        import time

        # Create mock cache structure with multiple snapshots
        model_dir = tmp_path / "models--mlx-community--multi-model" / "snapshots"
        old_snapshot = model_dir / "old123"
        new_snapshot = model_dir / "new456"
        old_snapshot.mkdir(parents=True)
        time.sleep(0.01)  # Ensure different mtime
        new_snapshot.mkdir(parents=True)

        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            result = get_local_model_path("mlx-community/multi-model")
            assert result == new_snapshot


class TestReadModelConfig:
    """Tests for read_model_config function."""

    def test_returns_none_when_model_not_downloaded(self, tmp_path):
        """Test returns None when model is not downloaded."""
        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            result = read_model_config("mlx-community/nonexistent")
            assert result is None

    def test_returns_none_when_config_not_exists(self, tmp_path):
        """Test returns None when config.json doesn't exist."""
        # Create model directory without config.json
        model_dir = tmp_path / "models--mlx-community--no-config" / "snapshots" / "abc"
        model_dir.mkdir(parents=True)

        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            result = read_model_config("mlx-community/no-config")
            assert result is None

    def test_returns_config_when_exists(self, tmp_path):
        """Test returns parsed config.json contents."""
        # Create model directory with config.json
        model_dir = tmp_path / "models--mlx-community--has-config" / "snapshots" / "abc"
        model_dir.mkdir(parents=True)
        config = {"model_type": "minimax", "architectures": ["MinimaxLMForCausalLM"]}
        (model_dir / "config.json").write_text(json.dumps(config))

        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            result = read_model_config("mlx-community/has-config")
            assert result == config

    def test_returns_none_on_invalid_json(self, tmp_path):
        """Test returns None when config.json is invalid."""
        model_dir = tmp_path / "models--mlx-community--bad-json" / "snapshots" / "abc"
        model_dir.mkdir(parents=True)
        (model_dir / "config.json").write_text("not valid json")

        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            result = read_model_config("mlx-community/bad-json")
            assert result is None


class TestDetectModelFamily:
    """Tests for detect_model_family function."""

    def test_detects_minimax_from_model_type(self, tmp_path):
        """Test detects MiniMax from model_type field."""
        model_dir = tmp_path / "models--mlx-community--MiniMax-M2" / "snapshots" / "abc"
        model_dir.mkdir(parents=True)
        config = {"model_type": "minimax"}
        (model_dir / "config.json").write_text(json.dumps(config))

        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            result = detect_model_family("mlx-community/MiniMax-M2")
            assert result == "minimax"

    def test_detects_qwen_from_model_type(self, tmp_path):
        """Test detects Qwen from model_type field."""
        model_dir = tmp_path / "models--mlx-community--Qwen3-8B" / "snapshots" / "abc"
        model_dir.mkdir(parents=True)
        config = {"model_type": "qwen2"}
        (model_dir / "config.json").write_text(json.dumps(config))

        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            # Note: qwen2 contains "qwen" so it should match qwen3 family
            result = detect_model_family("mlx-community/Qwen3-8B")
            assert result == "qwen3"

    def test_detects_glm_from_architectures(self, tmp_path):
        """Test detects GLM from architectures field."""
        model_dir = tmp_path / "models--mlx-community--GLM4-9B" / "snapshots" / "abc"
        model_dir.mkdir(parents=True)
        config = {"architectures": ["Glm4ForCausalLM"]}
        (model_dir / "config.json").write_text(json.dumps(config))

        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            result = detect_model_family("mlx-community/GLM4-9B")
            assert result == "glm"

    def test_detects_from_model_path_fallback(self, tmp_path):
        """Test detects from model path when not downloaded."""
        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            # Model not downloaded, should fall back to path matching
            result = detect_model_family("mlx-community/MiniMax-M2.1-3bit")
            assert result == "minimax"

    def test_detects_qwen_from_path(self, tmp_path):
        """Test detects Qwen from model path."""
        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            result = detect_model_family("mlx-community/Qwen2.5-72B-4bit")
            assert result == "qwen3"

    def test_detects_qwen_coder_from_path(self, tmp_path):
        """Test detects Qwen Coder from model path."""
        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            result = detect_model_family("mlx-community/Qwen2.5-Coder-32B-4bit")
            assert result == "qwen3_coder"

    def test_returns_none_for_unknown_model(self, tmp_path):
        """Test returns None for unknown model family."""
        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            result = detect_model_family("mlx-community/Llama-3-8B-4bit")
            assert result is None


class TestGetParserOptions:
    """Tests for get_parser_options function.

    NOTE: Parser options are deprecated with the embedded MLX Server.
    """

    def test_returns_empty_dict_always(self, tmp_path):
        """Parser options always return empty dict (feature deprecated)."""
        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            assert get_parser_options("mlx-community/MiniMax-M2.1-3bit") == {}
            assert get_parser_options("mlx-community/Llama-3-8B") == {}


class TestGetModelDetectionInfo:
    """Tests for get_model_detection_info function."""

    def test_returns_full_detection_info(self, tmp_path):
        """Test returns complete detection info."""
        # Create downloaded model
        model_dir = tmp_path / "models--mlx-community--MiniMax-M2" / "snapshots" / "abc"
        model_dir.mkdir(parents=True)
        config = {"model_type": "minimax"}
        (model_dir / "config.json").write_text(json.dumps(config))

        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            result = get_model_detection_info("mlx-community/MiniMax-M2")

            assert result["model_family"] == "minimax"
            # Parser options are empty now (feature deprecated)
            assert result["recommended_options"] == {}
            assert result["is_downloaded"] is True

    def test_returns_info_for_not_downloaded(self, tmp_path):
        """Test returns info for model not downloaded."""
        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            result = get_model_detection_info("mlx-community/MiniMax-M2.1-3bit")

            assert result["model_family"] == "minimax"
            assert result["is_downloaded"] is False

    def test_returns_info_for_unknown_model(self, tmp_path):
        """Test returns info for unknown model family."""
        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            result = get_model_detection_info("mlx-community/Unknown-Model")

            assert result["model_family"] is None
            assert result["recommended_options"] == {}
            assert result["is_downloaded"] is False


class TestVersionParsing:
    """Tests for version parsing utilities."""

    def test_parse_version_simple(self):
        """Test parsing simple version string."""
        assert parse_version("0.28.4") == (0, 28, 4)

    def test_parse_version_two_parts(self):
        """Test parsing two-part version string."""
        assert parse_version("1.0") == (1, 0)

    def test_parse_version_single(self):
        """Test parsing single version number."""
        assert parse_version("2") == (2,)

    def test_parse_version_invalid(self):
        """Test parsing invalid version returns (0,)."""
        assert parse_version("invalid") == (0,)


class TestGetMlxLmVersion:
    """Tests for get_mlx_lm_version function."""

    def test_returns_version_when_installed(self):
        """Test returns version string when mlx-lm is installed."""
        with patch("mlx_manager.utils.model_detection.version") as mock_version:
            mock_version.return_value = "0.30.2"
            result = get_mlx_lm_version()
            assert result == "0.30.2"
            mock_version.assert_called_once_with("mlx-lm")

    def test_returns_none_when_not_installed(self):
        """Test returns None when mlx-lm is not installed."""
        from importlib.metadata import PackageNotFoundError

        with patch(
            "mlx_manager.utils.model_detection.version",
            side_effect=PackageNotFoundError(),
        ):
            result = get_mlx_lm_version()
            assert result is None


class TestCheckMlxLmSupport:
    """Tests for check_mlx_lm_support function."""

    def test_minimax_supported_with_new_version(self):
        """Test MiniMax is supported with mlx-lm >= 0.28.4."""
        with patch(
            "mlx_manager.utils.model_detection.get_mlx_lm_version",
            return_value="0.30.2",
        ):
            result = check_mlx_lm_support("minimax")
            assert result["supported"] is True
            assert result["installed_version"] == "0.30.2"
            assert result["required_version"] == "0.28.4"

    def test_minimax_not_supported_with_old_version(self):
        """Test MiniMax is not supported with old mlx-lm."""
        with patch(
            "mlx_manager.utils.model_detection.get_mlx_lm_version",
            return_value="0.28.0",
        ):
            result = check_mlx_lm_support("minimax")
            assert result["supported"] is False
            assert result["installed_version"] == "0.28.0"
            assert result["required_version"] == "0.28.4"
            assert "error" in result
            assert "0.28.4" in result["error"]
            assert "upgrade_command" in result

    def test_minimax_exact_version_supported(self):
        """Test MiniMax is supported with exact minimum version."""
        with patch(
            "mlx_manager.utils.model_detection.get_mlx_lm_version",
            return_value="0.28.4",
        ):
            result = check_mlx_lm_support("minimax")
            assert result["supported"] is True

    def test_not_installed_returns_error(self):
        """Test returns error when mlx-lm is not installed."""
        with patch(
            "mlx_manager.utils.model_detection.get_mlx_lm_version",
            return_value=None,
        ):
            result = check_mlx_lm_support("minimax")
            assert result["supported"] is False
            assert result["installed_version"] is None
            assert "error" in result
            assert "not installed" in result["error"]

    def test_unknown_family_always_supported(self):
        """Test unknown model families have no version requirement."""
        with patch(
            "mlx_manager.utils.model_detection.get_mlx_lm_version",
            return_value="0.20.0",
        ):
            result = check_mlx_lm_support("unknown_family")
            assert result["supported"] is True
            assert result["required_version"] is None

    def test_qwen_no_version_requirement(self):
        """Test Qwen has no minimum version requirement."""
        with patch(
            "mlx_manager.utils.model_detection.get_mlx_lm_version",
            return_value="0.20.0",
        ):
            result = check_mlx_lm_support("qwen")
            assert result["supported"] is True


class TestModelFamilyMinVersions:
    """Tests for MODEL_FAMILY_MIN_VERSIONS constant."""

    def test_minimax_has_version_requirement(self):
        """Test MiniMax has a minimum version requirement."""
        assert "minimax" in MODEL_FAMILY_MIN_VERSIONS
        assert MODEL_FAMILY_MIN_VERSIONS["minimax"] == "0.28.4"


# ============================================================================
# Tests for get_local_model_path exception handling (lines 137-139)
# ============================================================================


class TestGetLocalModelPathExceptions:
    """Tests for get_local_model_path exception handling."""

    def test_get_local_model_path_stat_exception(self, tmp_path):
        """Test handles exception when reading model snapshots (line 137-139)."""
        # Create model directory with snapshots
        model_dir = tmp_path / "models--mlx-community--test-model" / "snapshots"
        snapshot_dir = model_dir / "abc123"
        snapshot_dir.mkdir(parents=True)

        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path

            # Mock sorted to raise an exception during iteration
            original_sorted = sorted

            def mock_sorted(iterable, **kwargs):
                # Raise when trying to sort (during stat access)
                if "key" in kwargs:
                    raise OSError("Permission denied")
                return original_sorted(iterable, **kwargs)

            with patch("mlx_manager.utils.model_detection.sorted", side_effect=mock_sorted):
                result = get_local_model_path("mlx-community/test-model")

        # Should return None and log warning
        assert result is None


# ============================================================================
# Tests for detect_model_family additional variants (lines 196, 198, 211, 215, 219, 223)
# ============================================================================


class TestDetectModelFamilyVariants:
    """Tests for additional model family detection variants."""

    def test_detects_qwen_moe_from_path(self, tmp_path):
        """Test detects Qwen MOE variant from model path (line 196)."""
        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            result = detect_model_family("mlx-community/Qwen2.5-MoE-72B-4bit")
            assert result == "qwen3_moe"

    def test_detects_qwen_vl_from_path(self, tmp_path):
        """Test detects Qwen VL variant from model path (line 198)."""
        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            result = detect_model_family("mlx-community/Qwen2-VL-7B-4bit")
            assert result == "qwen3_vl"

    def test_detects_nemotron_from_path(self, tmp_path):
        """Test detects Nemotron from model path (line 211)."""
        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            result = detect_model_family("mlx-community/Nemotron-3-8B-4bit")
            assert result == "nemotron"

    def test_detects_harmony_from_path(self, tmp_path):
        """Test detects Harmony from model path (line 215)."""
        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            result = detect_model_family("mlx-community/Harmony-LLM-7B-4bit")
            assert result == "harmony"

    def test_detects_hermes_from_path(self, tmp_path):
        """Test detects Hermes from model path (line 219)."""
        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            result = detect_model_family("mlx-community/Hermes-3-Llama-3.1-8B-4bit")
            assert result == "hermes"

    def test_detects_solar_from_path(self, tmp_path):
        """Test detects Solar from model path (line 223)."""
        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            result = detect_model_family("mlx-community/Solar-10.7B-4bit")
            assert result == "solar"

    def test_detects_nemotron_from_config(self, tmp_path):
        """Test detects Nemotron from config.json model_type."""
        model_dir = tmp_path / "models--mlx-community--Nemotron" / "snapshots" / "abc"
        model_dir.mkdir(parents=True)
        config = {"model_type": "nemotron"}
        (model_dir / "config.json").write_text(json.dumps(config))

        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            result = detect_model_family("mlx-community/Nemotron")
            assert result == "nemotron"

    def test_detects_harmony_from_architectures(self, tmp_path):
        """Test detects Harmony from architectures field."""
        model_dir = tmp_path / "models--mlx-community--HarmonyModel" / "snapshots" / "abc"
        model_dir.mkdir(parents=True)
        config = {"architectures": ["HarmonyForCausalLM"]}
        (model_dir / "config.json").write_text(json.dumps(config))

        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            result = detect_model_family("mlx-community/HarmonyModel")
            assert result == "harmony"

    def test_detects_hermes_from_config(self, tmp_path):
        """Test detects Hermes from config.json model_type."""
        model_dir = tmp_path / "models--mlx-community--HermesModel" / "snapshots" / "abc"
        model_dir.mkdir(parents=True)
        config = {"model_type": "hermes"}
        (model_dir / "config.json").write_text(json.dumps(config))

        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            result = detect_model_family("mlx-community/HermesModel")
            assert result == "hermes"

    def test_detects_solar_from_config(self, tmp_path):
        """Test detects Solar from config.json model_type."""
        model_dir = tmp_path / "models--mlx-community--SolarModel" / "snapshots" / "abc"
        model_dir.mkdir(parents=True)
        config = {"model_type": "solar_pro"}
        (model_dir / "config.json").write_text(json.dumps(config))

        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            result = detect_model_family("mlx-community/SolarModel")
            assert result == "solar"


# ============================================================================
# Tests for ModelCharacteristics extraction (Phase 4)
# ============================================================================


class TestExtractCharacteristics:
    """Tests for extract_characteristics function."""

    def test_extracts_all_fields_from_complete_config(self):
        """Test extracting all fields from a complete config."""
        config = {
            "model_type": "qwen2",
            "architectures": ["Qwen2ForCausalLM"],
            "max_position_embeddings": 32768,
            "num_hidden_layers": 28,
            "hidden_size": 3584,
            "vocab_size": 152064,
            "num_attention_heads": 28,
            "num_key_value_heads": 4,
            "use_cache": True,
            "quantization": {"bits": 4, "group_size": 64},
        }
        chars = extract_characteristics(config)

        assert chars.model_type == "qwen2"
        assert chars.architecture_family == "Qwen"
        assert chars.max_position_embeddings == 32768
        assert chars.num_hidden_layers == 28
        assert chars.hidden_size == 3584
        assert chars.vocab_size == 152064
        assert chars.num_attention_heads == 28
        assert chars.num_key_value_heads == 4
        assert chars.quantization_bits == 4
        assert chars.quantization_group_size == 64
        assert chars.is_multimodal is False
        assert chars.multimodal_type is None
        assert chars.use_cache is True

    def test_extracts_minimal_config(self):
        """Test extracting from minimal config with only model_type."""
        config = {"model_type": "llama"}
        chars = extract_characteristics(config)

        assert chars.model_type == "llama"
        assert chars.architecture_family == "Llama"
        assert chars.is_multimodal is False
        assert chars.max_position_embeddings is None
        assert chars.quantization_bits is None

    def test_handles_missing_fields_gracefully(self):
        """Test handles missing fields with defaults."""
        config = {}
        chars = extract_characteristics(config)

        assert chars.model_type == ""
        assert chars.architecture_family == "Unknown"
        assert chars.is_multimodal is False
        assert chars.use_cache is True

    def test_handles_quantization_without_bits(self):
        """Test handles quantization config without bits field."""
        config = {
            "model_type": "llama",
            "quantization": {"group_size": 64},
        }
        chars = extract_characteristics(config)

        assert chars.quantization_bits is None
        assert chars.quantization_group_size == 64

    def test_handles_non_dict_quantization(self):
        """Test handles quantization as non-dict value."""
        config = {
            "model_type": "llama",
            "quantization": "4bit",  # String instead of dict
        }
        chars = extract_characteristics(config)

        assert chars.quantization_bits is None
        assert chars.quantization_group_size is None


class TestDetectMultimodal:
    """Tests for detect_multimodal function."""

    def test_detects_vision_config_as_multimodal(self):
        """Test vision_config key indicates multimodal."""
        config = {"model_type": "qwen2_vl", "vision_config": {"image_size": 224}}
        is_mm, mm_type = detect_multimodal(config)
        assert is_mm is True
        assert mm_type == "vision"

    def test_detects_image_token_id_as_multimodal(self):
        """Test image_token_id key indicates multimodal."""
        config = {"model_type": "llava", "image_token_id": 32000}
        is_mm, mm_type = detect_multimodal(config)
        assert is_mm is True
        assert mm_type == "vision"

    def test_detects_video_token_id_as_multimodal(self):
        """Test video_token_id key indicates multimodal."""
        config = {"model_type": "video_llm", "video_token_id": 32001}
        is_mm, mm_type = detect_multimodal(config)
        assert is_mm is True
        assert mm_type == "vision"

    def test_detects_image_token_index_as_multimodal(self):
        """Test image_token_index key indicates multimodal (Gemma 3 style)."""
        config = {"model_type": "gemma3", "image_token_index": 262144}
        is_mm, mm_type = detect_multimodal(config)
        assert is_mm is True
        assert mm_type == "vision"

    def test_detects_vl_in_model_type(self):
        """Test 'vl' in model_type indicates multimodal."""
        config = {"model_type": "qwen2_vl"}
        is_mm, mm_type = detect_multimodal(config)
        assert is_mm is True
        assert mm_type == "vision"

    def test_detects_vision_in_model_type(self):
        """Test 'vision' in model_type indicates multimodal."""
        config = {"model_type": "llama_vision"}
        is_mm, mm_type = detect_multimodal(config)
        assert is_mm is True
        assert mm_type == "vision"

    def test_detects_multimodal_in_architectures(self):
        """Test 'multimodal' in architectures indicates multimodal."""
        config = {"architectures": ["MultimodalLlamaForCausalLM"]}
        is_mm, mm_type = detect_multimodal(config)
        assert is_mm is True
        assert mm_type == "vision"

    def test_detects_vision_in_architectures(self):
        """Test 'vision' in architectures indicates multimodal."""
        config = {"architectures": ["VisionLlamaForCausalLM"]}
        is_mm, mm_type = detect_multimodal(config)
        assert is_mm is True
        assert mm_type == "vision"

    def test_text_only_model(self):
        """Test text-only model returns False."""
        config = {"model_type": "llama", "architectures": ["LlamaForCausalLM"]}
        is_mm, mm_type = detect_multimodal(config)
        assert is_mm is False
        assert mm_type is None

    def test_empty_config(self):
        """Test empty config returns False."""
        config = {}
        is_mm, mm_type = detect_multimodal(config)
        assert is_mm is False
        assert mm_type is None


class TestNormalizeArchitecture:
    """Tests for normalize_architecture function."""

    def test_normalizes_qwen2_to_qwen(self):
        """Test qwen2 normalizes to Qwen."""
        config = {"model_type": "qwen2"}
        assert normalize_architecture(config) == "Qwen"

    def test_normalizes_llama_to_llama(self):
        """Test llama normalizes to Llama."""
        config = {"model_type": "llama"}
        assert normalize_architecture(config) == "Llama"

    def test_normalizes_mistral_to_mistral(self):
        """Test mistral normalizes to Mistral."""
        config = {"model_type": "mistral"}
        assert normalize_architecture(config) == "Mistral"

    def test_normalizes_gemma2_to_gemma(self):
        """Test gemma2 normalizes to Gemma."""
        config = {"model_type": "gemma2"}
        assert normalize_architecture(config) == "Gemma"

    def test_normalizes_phi3_to_phi(self):
        """Test phi3 normalizes to Phi."""
        config = {"model_type": "phi3"}
        assert normalize_architecture(config) == "Phi"

    def test_partial_match_qwen2_vl(self):
        """Test partial match for qwen2_vl."""
        config = {"model_type": "qwen2_vl"}
        assert normalize_architecture(config) == "Qwen"

    def test_fallback_to_architectures(self):
        """Test falls back to architectures field."""
        config = {"architectures": ["DeepseekV3ForCausalLM"]}
        assert normalize_architecture(config) == "DeepSeek"

    def test_returns_unknown_for_unrecognized(self):
        """Test returns Unknown for unrecognized model."""
        config = {"model_type": "totally_new_model"}
        assert normalize_architecture(config) == "Unknown"

    def test_returns_unknown_for_empty_config(self):
        """Test returns Unknown for empty config."""
        config = {}
        assert normalize_architecture(config) == "Unknown"


class TestExtractCharacteristicsFromModel:
    """Tests for extract_characteristics_from_model function."""

    def test_returns_characteristics_for_downloaded_model(self, tmp_path):
        """Test returns characteristics when model is downloaded."""
        model_dir = tmp_path / "models--mlx-community--test-model" / "snapshots" / "abc"
        model_dir.mkdir(parents=True)
        config = {
            "model_type": "qwen2",
            "max_position_embeddings": 32768,
            "num_hidden_layers": 28,
            "quantization": {"bits": 4},
        }
        (model_dir / "config.json").write_text(json.dumps(config))

        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            result = extract_characteristics_from_model("mlx-community/test-model")

        assert result is not None
        assert result.architecture_family == "Qwen"
        assert result.max_position_embeddings == 32768
        assert result.quantization_bits == 4

    def test_returns_none_when_model_not_downloaded(self, tmp_path):
        """Test returns None when model is not downloaded."""
        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            result = extract_characteristics_from_model("mlx-community/nonexistent")

        assert result is None

    def test_returns_none_when_config_missing(self, tmp_path):
        """Test returns None when config.json is missing."""
        model_dir = tmp_path / "models--mlx-community--no-config" / "snapshots" / "abc"
        model_dir.mkdir(parents=True)
        # No config.json file

        with patch("mlx_manager.utils.model_detection.settings") as mock_settings:
            mock_settings.hf_cache_path = tmp_path
            result = extract_characteristics_from_model("mlx-community/no-config")

        assert result is None


class TestArchitectureFamiliesMapping:
    """Tests for ARCHITECTURE_FAMILIES constant."""

    def test_has_major_architectures(self):
        """Test includes all major model architectures."""
        expected = [
            "llama",
            "qwen",
            "mistral",
            "gemma",
            "phi",
            "deepseek",
            "glm",
            "minimax",
            "falcon",
        ]
        for arch in expected:
            assert arch in ARCHITECTURE_FAMILIES, f"Missing architecture: {arch}"

    def test_maps_to_display_names(self):
        """Test maps to proper display names."""
        assert ARCHITECTURE_FAMILIES["llama"] == "Llama"
        assert ARCHITECTURE_FAMILIES["qwen2"] == "Qwen"
        assert ARCHITECTURE_FAMILIES["mistral"] == "Mistral"
        assert ARCHITECTURE_FAMILIES["deepseek_v3"] == "DeepSeek"
        assert ARCHITECTURE_FAMILIES["minimax"] == "MiniMax"


class TestQuantizationExtraction:
    """Tests for quantization info extraction."""

    def test_extracts_4bit_quantization(self):
        """Test extracts 4-bit quantization."""
        config = {"quantization": {"bits": 4, "group_size": 64}}
        chars = extract_characteristics(config)
        assert chars.quantization_bits == 4
        assert chars.quantization_group_size == 64

    def test_extracts_8bit_quantization(self):
        """Test extracts 8-bit quantization."""
        config = {"quantization": {"bits": 8}}
        chars = extract_characteristics(config)
        assert chars.quantization_bits == 8

    def test_extracts_3bit_quantization(self):
        """Test extracts 3-bit quantization."""
        config = {"quantization": {"bits": 3, "group_size": 128}}
        chars = extract_characteristics(config)
        assert chars.quantization_bits == 3

    def test_no_quantization_returns_none(self):
        """Test no quantization config returns None for bits."""
        config = {"model_type": "llama"}
        chars = extract_characteristics(config)
        assert chars.quantization_bits is None

    def test_empty_quantization_returns_none(self):
        """Test empty quantization dict returns None for bits."""
        config = {"quantization": {}}
        chars = extract_characteristics(config)
        assert chars.quantization_bits is None
