from __future__ import annotations

import json
from pathlib import Path
import ssl
import tomllib
from typing import Literal, TypedDict, Unpack
from unittest.mock import MagicMock, patch

import pytest
import tomli_w

from tests.conftest import build_test_vibe_config
from vibe.core.config import ModelConfig, ProviderConfig, VibeConfig
from vibe.core.config._settings import (
    DEFAULT_MISTRAL_BROWSER_AUTH_API_BASE_URL,
    DEFAULT_MISTRAL_BROWSER_AUTH_BASE_URL,
    DEFAULT_PROVIDERS,
)
from vibe.core.config.harness_files import (
    HarnessFilesManager,
    init_harness_files_manager,
    reset_harness_files_manager,
)
from vibe.core.paths import VIBE_HOME
from vibe.core.trusted_folders import trusted_folders_manager
from vibe.core.types import Backend
from vibe.core.utils.http import build_ssl_context, configure_ssl_context
from vibe.setup.onboarding.context import OnboardingContext


class _ProviderConfigOverrides(TypedDict, total=False):
    api_key_env_var: str
    browser_auth_base_url: str | None
    browser_auth_api_base_url: str | None
    api_style: str
    backend: Backend
    reasoning_field_name: str
    project_id: str
    region: str


class _ModelConfigOverrides(TypedDict, total=False):
    temperature: float
    input_price: float
    output_price: float
    thinking: Literal["off", "low", "medium", "high"]
    auto_compact_threshold: int


def _default_provider(name: str) -> ProviderConfig:
    return next(provider for provider in DEFAULT_PROVIDERS if provider.name == name)


def _custom_provider(**overrides: Unpack[_ProviderConfigOverrides]) -> ProviderConfig:
    return ProviderConfig(
        name="custom-provider", api_base="https://custom.example/v1", **overrides
    )


def _custom_model(**overrides: Unpack[_ModelConfigOverrides]) -> ModelConfig:
    return ModelConfig(
        name="custom-model",
        provider="custom-provider",
        alias="custom-model",
        **overrides,
    )


def _custom_provider_payload(**overrides: object) -> dict[str, object]:
    payload: dict[str, object] = {
        "name": "custom-provider",
        "api_base": "https://custom.example/v1",
        "api_key_env_var": "CUSTOM_API_KEY",
    }
    payload.update(overrides)
    return payload


def _custom_model_payload(**overrides: object) -> dict[str, object]:
    payload: dict[str, object] = {
        "name": "custom-model",
        "provider": "custom-provider",
        "alias": "custom-model",
    }
    payload.update(overrides)
    return payload


class TestResolveConfigFile:
    def test_resolves_local_config_when_exists_and_folder_is_trusted(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.chdir(tmp_path)
        local_config_dir = tmp_path / ".vibe"
        local_config_dir.mkdir()
        local_config = local_config_dir / "config.toml"
        local_config.write_text('active_model = "test"', encoding="utf-8")

        monkeypatch.setattr(trusted_folders_manager, "is_trusted", lambda _: True)

        reset_harness_files_manager()
        init_harness_files_manager("user", "project")
        from vibe.core.config.harness_files import get_harness_files_manager

        mgr = get_harness_files_manager()
        resolved = mgr.config_file
        assert resolved is not None
        assert resolved == local_config
        assert resolved.is_file()
        assert resolved.read_text(encoding="utf-8") == 'active_model = "test"'

    def test_resolves_global_config_when_folder_is_not_trusted(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.chdir(tmp_path)
        local_config_dir = tmp_path / ".vibe"
        local_config_dir.mkdir()
        local_config = local_config_dir / "config.toml"
        local_config.write_text('active_model = "test"', encoding="utf-8")

        reset_harness_files_manager()
        init_harness_files_manager("user", "project")
        from vibe.core.config.harness_files import get_harness_files_manager

        mgr = get_harness_files_manager()
        assert mgr.config_file == VIBE_HOME.path / "config.toml"

    def test_falls_back_to_global_config_when_local_missing(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.chdir(tmp_path)
        # Ensure no local config exists
        assert not (tmp_path / ".vibe" / "config.toml").exists()

        reset_harness_files_manager()
        init_harness_files_manager("user", "project")
        from vibe.core.config.harness_files import get_harness_files_manager

        mgr = get_harness_files_manager()
        assert mgr.config_file == VIBE_HOME.path / "config.toml"

    def test_respects_vibe_home_env_var(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        assert VIBE_HOME.path != tmp_path
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        assert VIBE_HOME.path == tmp_path

    def test_returns_none_when_no_sources(self) -> None:
        mgr = HarnessFilesManager(sources=())
        assert mgr.config_file is None

    def test_user_only_returns_global_config(self) -> None:
        mgr = HarnessFilesManager(sources=("user",))
        assert mgr.config_file == VIBE_HOME.path / "config.toml"


class TestSaveUpdates:
    def test_merges_nested_tool_updates_without_materializing_defaults(
        self, config_dir: Path
    ) -> None:
        config_file = config_dir / "config.toml"
        data = {"tools": {"bash": {"default_timeout": 600}}}
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        VibeConfig.save_updates({"tools": {"bash": {"permission": "always"}}})

        with config_file.open("rb") as f:
            result = tomllib.load(f)

        assert result == {
            "tools": {"bash": {"default_timeout": 600, "permission": "always"}}
        }

    def test_replaces_lists_instead_of_unioning_them(self, config_dir: Path) -> None:
        config_file = config_dir / "config.toml"
        data = {"installed_agents": ["lean", "other"]}
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        VibeConfig.save_updates({"installed_agents": ["lean"]})

        with config_file.open("rb") as f:
            result = tomllib.load(f)

        assert result["installed_agents"] == ["lean"]

    def test_prunes_nested_none_values_before_writing(self, config_dir: Path) -> None:
        config_file = config_dir / "config.toml"
        data = {"tools": {"bash": {"default_timeout": 600, "permission": "always"}}}
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        VibeConfig.save_updates({"tools": {"bash": {"permission": None}}})

        with config_file.open("rb") as f:
            result = tomllib.load(f)

        assert result == {"tools": {"bash": {"default_timeout": 600}}}


class TestSystemTrustStoreConfig:
    def test_load_configures_ssl_context_from_toml(self, config_dir: Path) -> None:
        config_file = config_dir / "config.toml"
        with config_file.open("rb") as f:
            data = tomllib.load(f)
        data["enable_system_trust_store"] = True
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        with patch("vibe.core.config._settings.configure_ssl_context") as configure:
            config = VibeConfig.load()

        assert config.enable_system_trust_store is True
        configure.assert_called_once_with(enable_system_trust_store=True)

    def test_load_configures_ssl_context_from_env(
        self, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_ENABLE_SYSTEM_TRUST_STORE", "true")

        with patch("vibe.core.config._settings.configure_ssl_context") as configure:
            config = VibeConfig.load()

        assert config.enable_system_trust_store is True
        configure.assert_called_once_with(enable_system_trust_store=True)

    def test_load_clears_cached_ssl_context_when_setting_changes(
        self, config_dir: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.delenv("SSL_CERT_FILE", raising=False)
        monkeypatch.delenv("SSL_CERT_DIR", raising=False)
        configure_ssl_context(enable_system_trust_store=False)
        build_ssl_context.cache_clear()

        try:
            config_file = config_dir / "config.toml"
            with config_file.open("rb") as f:
                data = tomllib.load(f)

            data["enable_system_trust_store"] = False
            with config_file.open("wb") as f:
                tomli_w.dump(data, f)

            default_ctx = MagicMock(spec=ssl.SSLContext)
            with patch(
                "vibe.core.utils.http.ssl.create_default_context",
                return_value=default_ctx,
            ):
                VibeConfig.load()
                assert build_ssl_context() is default_ctx

            data["enable_system_trust_store"] = True
            with config_file.open("wb") as f:
                tomli_w.dump(data, f)

            truststore_ctx = MagicMock(spec=ssl.SSLContext)
            with patch(
                "vibe.core.utils.http.truststore.SSLContext",
                return_value=truststore_ctx,
            ):
                VibeConfig.load()
                assert build_ssl_context() is truststore_ctx
        finally:
            configure_ssl_context(enable_system_trust_store=False)
            build_ssl_context.cache_clear()


class TestSetThinking:
    def test_persists_thinking_to_toml(self, config_dir: Path) -> None:
        config_file = config_dir / "config.toml"
        data = {
            "active_model": "my-model",
            "models": [
                {"name": "my-model", "provider": "mistral", "alias": "my-model"}
            ],
        }
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        cfg = VibeConfig.load()
        cfg.set_thinking("high")

        reloaded = VibeConfig.load()
        assert reloaded.get_active_model().thinking == "high"
        with config_file.open("rb") as f:
            result = tomllib.load(f)
        assert result["models"][0]["thinking"] == "high"

    def test_persists_thinking_for_correct_model(self, config_dir: Path) -> None:
        config_file = config_dir / "config.toml"
        data = {
            "active_model": "model-b",
            "models": [
                {"name": "model-a", "provider": "mistral", "alias": "model-a"},
                {"name": "model-b", "provider": "mistral", "alias": "model-b"},
            ],
        }
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        cfg = VibeConfig.load()
        cfg.set_thinking("max")

        with config_file.open("rb") as f:
            result = tomllib.load(f)
        assert result["models"][0].get("thinking") is None
        assert result["models"][1]["thinking"] == "max"

    def test_preserves_supports_images_when_materializing_defaults(
        self, config_dir: Path
    ) -> None:
        config_file = config_dir / "config.toml"
        data = {"active_model": "mistral-medium-3.5"}
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        cfg = VibeConfig.load()
        cfg.set_thinking("low")

        reloaded = VibeConfig.load()
        active = reloaded.get_active_model()
        assert active.alias == "mistral-medium-3.5"
        assert active.thinking == "low"
        assert active.supports_images is True
        with config_file.open("rb") as f:
            result = tomllib.load(f)
        active_entry = result["models"][0]
        assert active_entry["supports_images"] is True
        assert "temperature" not in active_entry
        assert "input_price" not in active_entry
        assert "output_price" not in active_entry
        assert "auto_compact_threshold" not in active_entry
        assert "supports_images" not in result["models"][1]


class TestMigrateLeavesFindInBashAllowlist:
    def test_keeps_find_in_config_file(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        data = {
            "applied_migrations": [VibeConfig._BASH_READ_ONLY_MIGRATION],
            "tools": {"bash": {"allowlist": ["echo", "ls"]}},
        }
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        reset_harness_files_manager()
        init_harness_files_manager("user")
        VibeConfig._migrate()

        with config_file.open("rb") as f:
            result = tomllib.load(f)
        assert result["tools"]["bash"]["allowlist"] == ["echo", "find", "ls"]

    def test_noop_when_find_already_present(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        data = {
            "applied_migrations": [VibeConfig._BASH_READ_ONLY_MIGRATION],
            "tools": {"bash": {"allowlist": ["echo", "find", "ls"]}},
        }
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        reset_harness_files_manager()
        init_harness_files_manager("user")
        VibeConfig._migrate()

        with config_file.open("rb") as f:
            result = tomllib.load(f)
        assert result["tools"]["bash"]["allowlist"] == ["echo", "find", "ls"]

    def test_noop_when_no_bash_tools_section(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        data = {"active_model": "test"}
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        reset_harness_files_manager()
        init_harness_files_manager("user")
        VibeConfig._migrate()

        with config_file.open("rb") as f:
            result = tomllib.load(f)
        assert "tools" not in result


class TestMigrateStripsBashAllowlistWildcardSuffix:
    def test_strips_trailing_wildcard_from_bash_allowlist_entries(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        data = {
            "applied_migrations": [VibeConfig._BASH_READ_ONLY_MIGRATION],
            "tools": {"bash": {"allowlist": ["git commit *", "npm install *", "echo"]}},
        }
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        reset_harness_files_manager()
        init_harness_files_manager("user")
        VibeConfig._migrate()

        with config_file.open("rb") as f:
            result = tomllib.load(f)
        assert result["tools"]["bash"]["allowlist"] == [
            "echo",
            "find",
            "git commit",
            "npm install",
        ]

    def test_dedupes_when_stripping_collides_with_existing_entry(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        data = {
            "applied_migrations": [VibeConfig._BASH_READ_ONLY_MIGRATION],
            "tools": {"bash": {"allowlist": ["git commit *", "git commit", "find"]}},
        }
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        reset_harness_files_manager()
        init_harness_files_manager("user")
        VibeConfig._migrate()

        with config_file.open("rb") as f:
            result = tomllib.load(f)
        assert result["tools"]["bash"]["allowlist"] == ["find", "git commit"]

    def test_noop_when_no_wildcard_suffix_present(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        data = {
            "applied_migrations": [VibeConfig._BASH_READ_ONLY_MIGRATION],
            "tools": {"bash": {"allowlist": ["echo", "find", "ls"]}},
        }
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        reset_harness_files_manager()
        init_harness_files_manager("user")
        VibeConfig._migrate()

        with config_file.open("rb") as f:
            result = tomllib.load(f)
        assert result["tools"]["bash"]["allowlist"] == ["echo", "find", "ls"]


class TestMigrateBashReadOnlyDefaults:
    def test_merges_read_only_commands_into_existing_allowlist(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        from vibe.core.tools.builtins.bash import default_read_only_commands

        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        data = {"tools": {"bash": {"allowlist": ["echo", "git commit"]}}}
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        reset_harness_files_manager()
        init_harness_files_manager("user")
        VibeConfig._migrate()

        with config_file.open("rb") as f:
            result = tomllib.load(f)
        allowlist = result["tools"]["bash"]["allowlist"]
        assert "git commit" in allowlist
        for cmd in default_read_only_commands():
            assert cmd in allowlist
        assert VibeConfig._BASH_READ_ONLY_MIGRATION in result["applied_migrations"]

    def test_does_not_readd_removed_command_after_migration(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        data = {
            "applied_migrations": [VibeConfig._BASH_READ_ONLY_MIGRATION],
            "tools": {"bash": {"allowlist": ["echo", "find"]}},
        }
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        reset_harness_files_manager()
        init_harness_files_manager("user")
        VibeConfig._migrate()

        with config_file.open("rb") as f:
            result = tomllib.load(f)
        assert result["tools"]["bash"]["allowlist"] == ["echo", "find"]

    def test_noop_when_no_bash_allowlist(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        data = {"active_model": "test"}
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        reset_harness_files_manager()
        init_harness_files_manager("user")
        VibeConfig._migrate()

        with config_file.open("rb") as f:
            result = tomllib.load(f)
        assert result == {"active_model": "test"}


class TestMigrateMistralVibeCliLatestDefaults:
    def test_updates_alias_temperature_and_thinking_for_default_model(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        data = {
            "models": [
                {
                    "name": "mistral-vibe-cli-latest",
                    "provider": "mistral",
                    "alias": "devstral-2",
                    "temperature": 0.2,
                    "thinking": "off",
                }
            ]
        }
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        reset_harness_files_manager()
        init_harness_files_manager("user")
        VibeConfig._migrate()

        with config_file.open("rb") as f:
            result = tomllib.load(f)
        assert result["models"][0]["alias"] == "mistral-medium-3.5"
        assert result["models"][0]["temperature"] == 1.0
        assert result["models"][0]["input_price"] == 1.5
        assert result["models"][0]["output_price"] == 7.5
        assert result["models"][0]["thinking"] == "high"

    def test_updates_active_model_when_devstral_2(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        data = {
            "active_model": "devstral-2",
            "models": [
                {
                    "name": "mistral-vibe-cli-latest",
                    "provider": "mistral",
                    "alias": "devstral-2",
                }
            ],
        }
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        reset_harness_files_manager()
        init_harness_files_manager("user")
        VibeConfig._migrate()

        with config_file.open("rb") as f:
            result = tomllib.load(f)
        assert result["active_model"] == "mistral-medium-3.5"

    def test_adds_temperature_and_thinking_when_missing(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        data = {
            "models": [
                {
                    "name": "mistral-vibe-cli-latest",
                    "provider": "mistral",
                    "alias": "devstral-2",
                }
            ]
        }
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        reset_harness_files_manager()
        init_harness_files_manager("user")
        VibeConfig._migrate()

        with config_file.open("rb") as f:
            result = tomllib.load(f)
        assert result["models"][0]["alias"] == "mistral-medium-3.5"
        assert result["models"][0]["temperature"] == 1.0
        assert result["models"][0]["input_price"] == 1.5
        assert result["models"][0]["output_price"] == 7.5
        assert result["models"][0]["thinking"] == "high"

    def test_skips_model_with_customized_alias(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        data = {
            "models": [
                {
                    "name": "mistral-vibe-cli-latest",
                    "provider": "mistral",
                    "alias": "my-custom-alias",
                    "temperature": 0.2,
                    "thinking": "off",
                }
            ]
        }
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        reset_harness_files_manager()
        init_harness_files_manager("user")
        VibeConfig._migrate()

        with config_file.open("rb") as f:
            result = tomllib.load(f)
        assert result["models"][0]["alias"] == "my-custom-alias"
        assert result["models"][0]["temperature"] == 0.2
        assert result["models"][0]["thinking"] == "off"

    def test_does_not_touch_other_models(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        data = {
            "models": [
                {
                    "name": "mistral-vibe-cli-latest",
                    "provider": "mistral",
                    "alias": "devstral-2",
                },
                {
                    "name": "other-model",
                    "provider": "mistral",
                    "alias": "devstral-2-clone",
                    "temperature": 0.5,
                    "thinking": "low",
                },
            ]
        }
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        reset_harness_files_manager()
        init_harness_files_manager("user")
        VibeConfig._migrate()

        with config_file.open("rb") as f:
            result = tomllib.load(f)
        assert result["models"][1]["alias"] == "devstral-2-clone"
        assert result["models"][1]["temperature"] == 0.5
        assert result["models"][1]["thinking"] == "low"

    def test_noop_when_no_models_section(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        data = {"theme": "dark"}
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        reset_harness_files_manager()
        init_harness_files_manager("user")
        VibeConfig._migrate()

        with config_file.open("rb") as f:
            result = tomllib.load(f)
        assert result == {"theme": "dark"}

    def test_idempotent_when_already_migrated(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        data = {
            "active_model": "mistral-medium-3.5",
            "models": [
                {
                    "name": "mistral-vibe-cli-latest",
                    "provider": "mistral",
                    "alias": "mistral-medium-3.5",
                    "temperature": 1.0,
                    "input_price": 1.5,
                    "output_price": 7.5,
                    "thinking": "high",
                }
            ],
        }
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        reset_harness_files_manager()
        init_harness_files_manager("user")
        VibeConfig._migrate()
        VibeConfig._migrate()

        with config_file.open("rb") as f:
            result = tomllib.load(f)
        assert result["active_model"] == "mistral-medium-3.5"
        assert result["models"][0]["alias"] == "mistral-medium-3.5"
        assert result["models"][0]["temperature"] == 1.0
        assert result["models"][0]["input_price"] == 1.5
        assert result["models"][0]["output_price"] == 7.5
        assert result["models"][0]["thinking"] == "high"

    def test_migrates_model_and_active_model_together(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        data = {
            "active_model": "devstral-2",
            "models": [
                {
                    "name": "mistral-vibe-cli-latest",
                    "provider": "mistral",
                    "alias": "devstral-2",
                }
            ],
        }
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        reset_harness_files_manager()
        init_harness_files_manager("user")
        VibeConfig._migrate()

        with config_file.open("rb") as f:
            result = tomllib.load(f)
        assert result["active_model"] == "mistral-medium-3.5"
        assert result["models"][0]["alias"] == "mistral-medium-3.5"
        assert result["models"][0]["temperature"] == 1.0
        assert result["models"][0]["input_price"] == 1.5
        assert result["models"][0]["output_price"] == 7.5
        assert result["models"][0]["thinking"] == "high"

    def test_backfills_supports_images_on_existing_mistral_medium_entry(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        data = {
            "active_model": "mistral-medium-3.5",
            "models": [
                {
                    "name": "mistral-vibe-cli-latest",
                    "provider": "mistral",
                    "alias": "mistral-medium-3.5",
                    "temperature": 1.0,
                    "input_price": 1.5,
                    "output_price": 7.5,
                    "thinking": "high",
                }
            ],
        }
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        reset_harness_files_manager()
        init_harness_files_manager("user")
        VibeConfig._migrate()

        with config_file.open("rb") as f:
            result = tomllib.load(f)
        assert result["models"][0]["supports_images"] is True

    def test_preserves_explicit_supports_images_false(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        data = {
            "models": [
                {
                    "name": "mistral-vibe-cli-latest",
                    "provider": "mistral",
                    "alias": "mistral-medium-3.5",
                    "supports_images": False,
                }
            ]
        }
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        reset_harness_files_manager()
        init_harness_files_manager("user")
        VibeConfig._migrate()

        with config_file.open("rb") as f:
            result = tomllib.load(f)
        assert result["models"][0]["supports_images"] is False


class TestAutoCompactThresholdFallback:
    def test_model_without_explicit_threshold_inherits_global(self) -> None:
        model = ModelConfig(name="m", provider="p", alias="m")
        cfg = build_test_vibe_config(
            auto_compact_threshold=42_000, models=[model], active_model="m"
        )
        assert cfg.get_active_model().auto_compact_threshold == 42_000

    def test_model_with_explicit_threshold_keeps_own_value(self) -> None:
        model = ModelConfig(
            name="m", provider="p", alias="m", auto_compact_threshold=99_000
        )
        cfg = build_test_vibe_config(
            auto_compact_threshold=42_000, models=[model], active_model="m"
        )
        assert cfg.get_active_model().auto_compact_threshold == 99_000

    def test_default_global_threshold_used_when_nothing_set(self) -> None:
        model = ModelConfig(name="m", provider="p", alias="m")
        cfg = build_test_vibe_config(models=[model], active_model="m")
        assert cfg.get_active_model().auto_compact_threshold == 200_000

    def test_changed_global_threshold_propagates_on_reload(self) -> None:
        model = ModelConfig(name="m", provider="p", alias="m")

        cfg1 = build_test_vibe_config(
            auto_compact_threshold=50_000, models=[model], active_model="m"
        )
        assert cfg1.get_active_model().auto_compact_threshold == 50_000

        # Simulate config reload with a different global threshold
        cfg2 = build_test_vibe_config(
            auto_compact_threshold=75_000, models=[model], active_model="m"
        )
        assert cfg2.get_active_model().auto_compact_threshold == 75_000


class TestDefaultProviderConfig:
    def test_default_mistral_provider_is_mistral_backend(self) -> None:
        provider = _default_provider("mistral")

        assert provider.name == "mistral"
        assert provider.backend.value == "mistral"
        assert provider.browser_auth_base_url == DEFAULT_MISTRAL_BROWSER_AUTH_BASE_URL
        assert (
            provider.browser_auth_api_base_url
            == DEFAULT_MISTRAL_BROWSER_AUTH_API_BASE_URL
        )
        assert provider.supports_browser_sign_in is True

    def test_non_mistral_provider_does_not_inherit_browser_auth_defaults(self) -> None:
        provider = _default_provider("llamacpp")

        assert provider.browser_auth_base_url is None
        assert provider.browser_auth_api_base_url is None
        assert provider.supports_browser_sign_in is False


class TestMistralBrowserAuthConfig:
    def test_provider_browser_auth_urls_are_dumped_when_set(self) -> None:
        cfg = build_test_vibe_config()
        provider = cfg.get_active_provider()
        dumped = cfg.model_dump(mode="json")

        assert provider.browser_auth_base_url == DEFAULT_MISTRAL_BROWSER_AUTH_BASE_URL
        assert (
            provider.browser_auth_api_base_url
            == DEFAULT_MISTRAL_BROWSER_AUTH_API_BASE_URL
        )
        assert (
            dumped["providers"][0]["browser_auth_base_url"]
            == DEFAULT_MISTRAL_BROWSER_AUTH_BASE_URL
        )
        assert (
            dumped["providers"][0]["browser_auth_api_base_url"]
            == DEFAULT_MISTRAL_BROWSER_AUTH_API_BASE_URL
        )

    def test_legacy_explicit_mistral_provider_backfills_browser_auth_urls_without_changing_backend(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        with config_file.open("wb") as f:
            tomli_w.dump(
                {
                    "active_model": "devstral-2",
                    "providers": [
                        {
                            "name": "mistral",
                            "api_base": "https://api.mistral.ai/v1",
                            "api_key_env_var": "MISTRAL_API_KEY",
                            "reasoning_field_name": "thoughts",
                        }
                    ],
                    "models": [
                        {
                            "name": "mistral-vibe-cli-latest",
                            "provider": "mistral",
                            "alias": "devstral-2",
                        }
                    ],
                },
                f,
            )

        reset_harness_files_manager()
        init_harness_files_manager("user")

        context = OnboardingContext.load()

        assert context.provider.name == "mistral"
        assert context.provider.backend.value == "generic"
        assert context.provider.reasoning_field_name == "thoughts"
        assert (
            context.provider.browser_auth_base_url
            == DEFAULT_MISTRAL_BROWSER_AUTH_BASE_URL
        )
        assert (
            context.provider.browser_auth_api_base_url
            == DEFAULT_MISTRAL_BROWSER_AUTH_API_BASE_URL
        )
        assert context.supports_browser_sign_in is True

    def test_legacy_explicit_mistral_provider_backfills_only_missing_browser_auth_url(
        self,
    ) -> None:
        provider = ProviderConfig(
            name="mistral",
            api_base="https://api.mistral.ai/v1",
            api_key_env_var="MISTRAL_API_KEY",
            browser_auth_base_url="https://custom-console.example",
        )

        assert provider.backend.value == "generic"
        assert provider.browser_auth_base_url == "https://custom-console.example"
        assert (
            provider.browser_auth_api_base_url
            == DEFAULT_MISTRAL_BROWSER_AUTH_API_BASE_URL
        )
        assert provider.supports_browser_sign_in is True

    def test_legacy_mistral_provider_keeps_browser_sign_in_after_round_trip(
        self,
    ) -> None:
        provider = ProviderConfig(
            name="mistral",
            api_base="https://api.mistral.ai/v1",
            api_key_env_var="MISTRAL_API_KEY",
        )

        reloaded_provider = ProviderConfig.model_validate(
            provider.model_dump(mode="json")
        )

        assert reloaded_provider.supports_browser_sign_in is True

    def test_explicit_generic_mistral_provider_does_not_get_browser_auth_defaults(
        self,
    ) -> None:
        provider = ProviderConfig(
            name="mistral",
            api_base="https://api.mistral.ai/v1",
            api_key_env_var="MISTRAL_API_KEY",
            backend=Backend.GENERIC,
        )

        assert provider.backend.value == "generic"
        assert provider.browser_auth_base_url is None
        assert provider.browser_auth_api_base_url is None
        assert provider.supports_browser_sign_in is False

    def test_custom_provider_browser_auth_urls_round_trip(self) -> None:
        custom_provider = _custom_provider(
            browser_auth_base_url="https://custom.example/sign-in",
            browser_auth_api_base_url="https://custom.example/api",
            backend=Backend.MISTRAL,
        )
        cfg = build_test_vibe_config(
            active_model="custom-model",
            providers=[custom_provider],
            models=[_custom_model()],
        )

        dumped = cfg.model_dump(mode="json")
        reloaded_provider = ProviderConfig.model_validate(dumped["providers"][0])

        assert (
            reloaded_provider.browser_auth_base_url == "https://custom.example/sign-in"
        )
        assert (
            reloaded_provider.browser_auth_api_base_url == "https://custom.example/api"
        )
        assert reloaded_provider.supports_browser_sign_in is True

    def test_custom_mistral_provider_without_browser_auth_urls_is_not_capable(
        self,
    ) -> None:
        provider = _custom_provider(backend=Backend.MISTRAL)

        assert provider.browser_auth_base_url is None
        assert provider.browser_auth_api_base_url is None
        assert provider.supports_browser_sign_in is False

    def test_non_mistral_provider_with_browser_auth_urls_is_not_capable(self) -> None:
        provider = _custom_provider(
            browser_auth_base_url="https://custom.example/sign-in",
            browser_auth_api_base_url="https://custom.example/api",
        )

        assert provider.supports_browser_sign_in is False


class TestOnboardingContextResolution:
    def test_load_uses_explicit_overrides_when_harness_manager_is_uninitialized(
        self,
    ) -> None:
        reset_harness_files_manager()

        context = OnboardingContext.load(
            active_model="custom-model",
            providers=[_custom_provider_payload()],
            models=[_custom_model_payload()],
        )

        assert context.provider.name == "custom-provider"
        assert context.provider.api_key_env_var == "CUSTOM_API_KEY"

    def test_load_uses_env_overrides(self, monkeypatch: pytest.MonkeyPatch) -> None:
        reset_harness_files_manager()
        monkeypatch.setenv("VIBE_ACTIVE_MODEL", "env-model")
        monkeypatch.setenv("VIBE_VIBE_BASE_URL", "https://env-vibe.example.com")
        monkeypatch.setenv(
            "VIBE_PROVIDERS",
            json.dumps([
                {
                    "name": "env-provider",
                    "api_base": "https://env.example/v1",
                    "api_key_env_var": "ENV_API_KEY",
                }
            ]),
        )
        monkeypatch.setenv(
            "VIBE_MODELS",
            json.dumps([
                {"name": "env-model", "provider": "env-provider", "alias": "env-model"}
            ]),
        )

        context = OnboardingContext.load()

        assert context.provider.name == "env-provider"
        assert context.provider.api_key_env_var == "ENV_API_KEY"
        assert context.vibe_base_url == "https://env-vibe.example.com"

    def test_load_prefers_explicit_overrides_over_toml_and_env(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        with config_file.open("wb") as file:
            tomli_w.dump(
                {
                    "active_model": "toml-model",
                    "providers": [
                        {
                            "name": "toml-provider",
                            "api_base": "https://toml.example/v1",
                            "api_key_env_var": "TOML_API_KEY",
                        }
                    ],
                    "models": [
                        {
                            "name": "toml-model",
                            "provider": "toml-provider",
                            "alias": "toml-model",
                        }
                    ],
                },
                file,
            )
        monkeypatch.setenv("VIBE_ACTIVE_MODEL", "env-model")
        monkeypatch.setenv(
            "VIBE_PROVIDERS",
            json.dumps([
                {
                    "name": "env-provider",
                    "api_base": "https://env.example/v1",
                    "api_key_env_var": "ENV_API_KEY",
                }
            ]),
        )
        monkeypatch.setenv(
            "VIBE_MODELS",
            json.dumps([
                {"name": "env-model", "provider": "env-provider", "alias": "env-model"}
            ]),
        )

        reset_harness_files_manager()
        init_harness_files_manager("user")

        context = OnboardingContext.load(
            active_model="custom-model",
            providers=[_custom_provider_payload()],
            models=[_custom_model_payload()],
        )

        assert context.provider.name == "custom-provider"
        assert context.provider.api_key_env_var == "CUSTOM_API_KEY"

    def test_load_accepts_typed_provider_and_model_overrides(self) -> None:
        context = OnboardingContext.load(
            active_model="custom-model",
            providers=[_custom_provider(api_key_env_var="CUSTOM_API_KEY")],
            models=[_custom_model()],
        )

        assert context.provider.name == "custom-provider"
        assert context.provider.api_key_env_var == "CUSTOM_API_KEY"

    def test_load_preserves_explicit_overrides_when_onboarding_toml_is_invalid(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        config_file.write_text("invalid = [", encoding="utf-8")

        reset_harness_files_manager()
        init_harness_files_manager("user")

        context = OnboardingContext.load(
            active_model="custom-model",
            providers=[_custom_provider_payload()],
            models=[_custom_model_payload()],
        )

        assert context.provider.name == "custom-provider"
        assert context.provider.api_key_env_var == "CUSTOM_API_KEY"

    def test_load_preserves_explicit_provider_and_model_overrides_when_toml_is_invalid(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        config_file.write_text("invalid = [", encoding="utf-8")

        reset_harness_files_manager()
        init_harness_files_manager("user")

        context = OnboardingContext.load(
            providers=[_custom_provider_payload()],
            models=[_custom_model_payload(alias="devstral-2")],
        )

        assert context.provider.name == "custom-provider"
        assert context.provider.api_key_env_var == "CUSTOM_API_KEY"

    def test_load_preserves_explicit_provider_override_when_toml_is_invalid(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        config_file.write_text("invalid = [", encoding="utf-8")

        reset_harness_files_manager()
        init_harness_files_manager("user")

        context = OnboardingContext.load(
            active_model="custom-model", providers=[_custom_provider_payload()]
        )

        assert context.provider.name == "custom-provider"
        assert context.provider.api_key_env_var == "CUSTOM_API_KEY"

    def test_load_preserves_explicit_overrides_when_onboarding_env_is_invalid(
        self, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        reset_harness_files_manager()
        monkeypatch.setenv("VIBE_PROVIDERS", "not-json")

        context = OnboardingContext.load(
            active_model="custom-model",
            providers=[_custom_provider_payload()],
            models=[_custom_model_payload()],
        )

        assert context.provider.name == "custom-provider"
        assert context.provider.api_key_env_var == "CUSTOM_API_KEY"

    def test_load_preserves_explicit_provider_override_when_onboarding_env_is_invalid(
        self, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        reset_harness_files_manager()
        monkeypatch.setenv("VIBE_MODELS", "not-json")

        context = OnboardingContext.load(
            active_model="custom-model", providers=[_custom_provider_payload()]
        )

        assert context.provider.name == "custom-provider"
        assert context.provider.api_key_env_var == "CUSTOM_API_KEY"

    def test_load_uses_valid_active_provider_when_unrelated_provider_is_malformed(
        self,
    ) -> None:
        context = OnboardingContext.load(
            active_model="custom-model",
            providers=[
                _custom_provider_payload(),
                {"name": "broken-provider", "backend": "mistral"},
            ],
            models=[_custom_model_payload()],
        )

        assert context.provider.name == "custom-provider"
        assert context.provider.api_key_env_var == "CUSTOM_API_KEY"

    def test_load_uses_valid_active_provider_when_unrelated_model_is_malformed(
        self,
    ) -> None:
        context = OnboardingContext.load(
            active_model="custom-model",
            providers=[_custom_provider_payload()],
            models=[
                _custom_model_payload(),
                {"name": "broken-model", "alias": "broken-model"},
            ],
        )

        assert context.provider.name == "custom-provider"
        assert context.provider.api_key_env_var == "CUSTOM_API_KEY"

    def test_load_uses_single_valid_provider_when_no_matching_model_exists(
        self,
    ) -> None:
        context = OnboardingContext.load(
            active_model="custom-model", providers=[_custom_provider_payload()]
        )

        assert context.provider.name == "custom-provider"
        assert context.provider.api_key_env_var == "CUSTOM_API_KEY"

    def test_load_preserves_browser_sign_in_for_valid_active_provider_with_unrelated_invalid_entry(
        self,
    ) -> None:
        context = OnboardingContext.load(
            active_model="custom-model",
            providers=[
                _custom_provider_payload(
                    browser_auth_base_url="https://custom.example/sign-in",
                    browser_auth_api_base_url="https://custom.example/api",
                    backend="mistral",
                ),
                {"name": "broken-provider", "backend": "mistral"},
            ],
            models=[_custom_model_payload()],
        )

        assert context.provider.name == "custom-provider"
        assert context.supports_browser_sign_in is True

    def test_load_falls_back_when_active_provider_is_invalid(self) -> None:
        context = OnboardingContext.load(
            active_model="custom-model",
            providers=[{"name": "custom-provider", "backend": "mistral"}],
            models=[_custom_model_payload()],
        )

        assert context.provider.name == "mistral"

    def test_load_falls_back_when_no_valid_provider_model_pair_exists(self) -> None:
        context = OnboardingContext.load(
            active_model="broken-model",
            providers=[{"name": "broken-provider", "backend": "mistral"}],
            models=[{"name": "broken-model", "alias": "broken-model"}],
        )

        assert context.provider.name == "mistral"

    def test_load_falls_back_when_onboarding_toml_is_invalid(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        config_file.write_text("invalid = [", encoding="utf-8")

        reset_harness_files_manager()
        init_harness_files_manager("user")

        context = OnboardingContext.load()

        assert context.provider.name == "mistral"

    def test_load_falls_back_when_onboarding_env_payload_is_invalid(
        self, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        reset_harness_files_manager()
        monkeypatch.setenv("VIBE_PROVIDERS", "not-json")

        context = OnboardingContext.load()

        assert context.provider.name == "mistral"


class TestCompactionModel:
    def test_get_compaction_model_returns_active_when_unset(self) -> None:
        cfg = build_test_vibe_config()
        assert cfg.get_compaction_model() == cfg.get_active_model()

    def test_get_compaction_model_returns_configured_model(self) -> None:
        compaction = ModelConfig(
            name="compact-model", provider="mistral", alias="compact"
        )
        cfg = build_test_vibe_config(compaction_model=compaction)
        assert cfg.get_compaction_model().name == "compact-model"

    def test_compaction_model_provider_must_match_active(self) -> None:
        from vibe.core.config import ProviderConfig

        compaction = ModelConfig(
            name="compact-model", provider="other", alias="compact"
        )
        providers = [
            ProviderConfig(
                name="mistral",
                api_base="https://api.mistral.ai/v1",
                api_key_env_var="MISTRAL_API_KEY",
            ),
            ProviderConfig(
                name="other",
                api_base="https://other.ai/v1",
                api_key_env_var="MISTRAL_API_KEY",
            ),
        ]
        with pytest.raises(ValueError, match="must share the same provider"):
            build_test_vibe_config(compaction_model=compaction, providers=providers)

    def test_compaction_model_provider_must_exist(self) -> None:
        compaction = ModelConfig(
            name="compact-model", provider="missing-provider", alias="compact"
        )
        with pytest.raises(
            ValueError,
            match="Provider 'missing-provider' for model 'compact-model' not found in configuration",
        ):
            build_test_vibe_config(compaction_model=compaction)

    def test_compaction_model_excluded_from_model_dump_when_none(self) -> None:
        cfg = build_test_vibe_config()
        dumped = cfg.model_dump()
        assert "compaction_model" not in dumped


class TestGetMistralProvider:
    def test_returns_active_provider_when_it_is_mistral(self) -> None:
        cfg = build_test_vibe_config()
        provider = cfg.get_mistral_provider()
        active = cfg.get_active_provider()
        assert provider is active
        assert provider is not None
        assert provider.backend == Backend.MISTRAL

    def test_falls_back_to_first_mistral_provider_when_active_is_not_mistral(
        self,
    ) -> None:
        mistral_provider = ProviderConfig(
            name="mistral",
            api_base="https://api.mistral.ai/v1",
            api_key_env_var="MISTRAL_API_KEY",
            backend=Backend.MISTRAL,
        )
        llamacpp_provider = ProviderConfig(
            name="llamacpp", api_base="http://127.0.0.1:8080/v1", api_key_env_var=""
        )
        llamacpp_model = ModelConfig(
            name="llama-local", provider="llamacpp", alias="llama-local"
        )
        cfg = build_test_vibe_config(
            providers=[llamacpp_provider, mistral_provider],
            models=[llamacpp_model],
            active_model="llama-local",
        )
        provider = cfg.get_mistral_provider()
        assert provider is mistral_provider

    def test_returns_none_when_no_mistral_provider(self) -> None:
        llamacpp_provider = ProviderConfig(
            name="llamacpp", api_base="http://127.0.0.1:8080/v1", api_key_env_var=""
        )
        llamacpp_model = ModelConfig(
            name="llama-local", provider="llamacpp", alias="llama-local"
        )
        cfg = build_test_vibe_config(
            providers=[llamacpp_provider],
            models=[llamacpp_model],
            active_model="llama-local",
        )
        assert cfg.get_mistral_provider() is None

    def test_falls_back_to_iterating_when_active_model_is_misconfigured(self) -> None:
        mistral_provider = ProviderConfig(
            name="mistral",
            api_base="https://api.mistral.ai/v1",
            api_key_env_var="MISTRAL_API_KEY",
            backend=Backend.MISTRAL,
        )
        llamacpp_model = ModelConfig(
            name="llama-local", provider="llamacpp", alias="llama-local"
        )
        cfg = build_test_vibe_config(
            providers=[mistral_provider],
            models=[llamacpp_model],
            active_model="llama-local",
        )
        provider = cfg.get_mistral_provider()
        assert provider is mistral_provider


class TestIsActiveModelMistral:
    def test_returns_true_when_active_provider_is_mistral(self) -> None:
        cfg = build_test_vibe_config()
        assert cfg.is_active_model_mistral() is True

    def test_returns_false_when_active_provider_is_not_mistral(self) -> None:
        cfg = build_test_vibe_config(
            providers=[
                ProviderConfig(
                    name="llamacpp",
                    api_base="http://127.0.0.1:8080/v1",
                    api_key_env_var="",
                )
            ],
            models=[
                ModelConfig(
                    name="llama-local", provider="llamacpp", alias="llama-local"
                )
            ],
            active_model="llama-local",
        )
        assert cfg.is_active_model_mistral() is False

    def test_returns_false_when_active_model_resolution_fails(self) -> None:
        cfg = build_test_vibe_config(
            providers=[
                ProviderConfig(
                    name="mistral",
                    api_base="https://api.mistral.ai/v1",
                    api_key_env_var="MISTRAL_API_KEY",
                    backend=Backend.MISTRAL,
                )
            ],
            models=[
                ModelConfig(
                    name="llama-local", provider="llamacpp", alias="llama-local"
                )
            ],
            active_model="llama-local",
        )
        assert cfg.is_active_model_mistral() is False


class TestMigrateRenamedTools:
    def test_renames_read_file_and_search_replace_keys(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        data = {
            "tools": {
                "read_file": {
                    "permission": "always",
                    "allowlist": ["src/**"],
                    "max_read_bytes": 64000,
                },
                "search_replace": {
                    "allowlist": ["src/**"],
                    "max_content_size": 100000,
                    "create_backup": True,
                },
            }
        }
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        reset_harness_files_manager()
        init_harness_files_manager("user")
        VibeConfig._migrate()

        with config_file.open("rb") as f:
            result = tomllib.load(f)
        tools = result["tools"]
        assert "read_file" not in tools
        assert "search_replace" not in tools
        assert tools["read"] == {
            "permission": "always",
            "allowlist": ["src/**"],
            "max_read_bytes": 64000,
        }
        # Common options carry over; edit-incompatible options are dropped.
        assert tools["edit"] == {"allowlist": ["src/**"]}

    def test_prefers_existing_new_key_and_drops_legacy(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        data = {
            "tools": {
                "read_file": {"permission": "always"},
                "read": {"permission": "ask"},
            }
        }
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        reset_harness_files_manager()
        init_harness_files_manager("user")
        VibeConfig._migrate()

        with config_file.open("rb") as f:
            result = tomllib.load(f)
        assert "read_file" not in result["tools"]
        assert result["tools"]["read"] == {"permission": "ask"}

    def test_renames_entries_in_enabled_and_disabled_lists(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        data = {
            "enabled_tools": ["read_file", "grep"],
            "disabled_tools": ["search_replace"],
        }
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        reset_harness_files_manager()
        init_harness_files_manager("user")
        VibeConfig._migrate()

        with config_file.open("rb") as f:
            result = tomllib.load(f)
        assert result["enabled_tools"] == ["read", "grep"]
        assert result["disabled_tools"] == ["edit"]

    def test_noop_when_no_legacy_tool_names(
        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        monkeypatch.setenv("VIBE_HOME", str(tmp_path))
        config_file = tmp_path / "config.toml"
        data = {"tools": {"read": {"permission": "always"}}}
        with config_file.open("wb") as f:
            tomli_w.dump(data, f)

        reset_harness_files_manager()
        init_harness_files_manager("user")
        VibeConfig._migrate()

        with config_file.open("rb") as f:
            result = tomllib.load(f)
        assert result["tools"] == {"read": {"permission": "always"}}
