from unittest.mock import MagicMock, patch


def test_create_timmy_custom_db_file():
    with (
        patch("timmy.agent.Agent"),
        patch("timmy.agent.Ollama"),
        patch("timmy.agent.SqliteDb") as MockDb,
        patch("timmy.agent._resolve_model_with_fallback", return_value=("llama3.2:3b", False)),
        patch("timmy.agent._check_model_available", return_value=True),
        patch("timmy.agent._warmup_model", return_value=True),
    ):
        from timmy.agent import create_timmy

        create_timmy(db_file="custom.db")

        MockDb.assert_called_once_with(db_file="custom.db")


def test_create_timmy_embeds_system_prompt():

    with (
        patch("timmy.agent.Agent") as MockAgent,
        patch("timmy.agent.Ollama"),
        patch("timmy.agent.SqliteDb"),
        patch("timmy.agent._resolve_model_with_fallback", return_value=("llama3.2:3b", False)),
        patch("timmy.agent._check_model_available", return_value=True),
        patch("timmy.agent._warmup_model", return_value=True),
    ):
        from timmy.agent import create_timmy

        create_timmy()

        kwargs = MockAgent.call_args.kwargs
        # Prompt should contain base system prompt (may have memory context appended)
        # Default model (llama3.2) uses the lite prompt
        assert "Timmy" in kwargs["description"]


# ── Ollama host regression (container connectivity) ─────────────────────────


def test_create_timmy_passes_ollama_url_to_model():
    """Regression: Ollama model must receive settings.ollama_url as host.

    Without this, containers default to localhost:11434 which is unreachable
    when Ollama runs on the Docker host.
    """
    with (
        patch("timmy.agent.Agent"),
        patch("timmy.agent.Ollama") as MockOllama,
        patch("timmy.agent.SqliteDb"),
        patch("timmy.agent._resolve_model_with_fallback", return_value=("llama3.2:3b", False)),
        patch("timmy.agent._check_model_available", return_value=True),
        patch("timmy.agent._warmup_model", return_value=True),
    ):
        from timmy.agent import create_timmy

        create_timmy()

        kwargs = MockOllama.call_args.kwargs
        assert "host" in kwargs, "Ollama() must receive host= parameter"
        from config import settings

        assert kwargs["host"] == settings.ollama_url


def test_create_timmy_respects_custom_ollama_url():
    """Ollama host should follow OLLAMA_URL when overridden in config."""
    custom_url = "http://host.docker.internal:11434"
    with (
        patch("timmy.agent.Agent"),
        patch("timmy.agent.Ollama") as MockOllama,
        patch("timmy.agent.SqliteDb"),
        patch("timmy.agent.settings") as mock_settings,
        patch("timmy.agent._resolve_model_with_fallback", return_value=("llama3.2:3b", False)),
        patch("timmy.agent._check_model_available", return_value=True),
        patch("timmy.agent._warmup_model", return_value=True),
    ):
        mock_settings.ollama_model = "llama3.2"
        mock_settings.ollama_url = custom_url
        mock_settings.ollama_num_ctx = 4096
        mock_settings.timmy_model_backend = "ollama"

        from timmy.agent import create_timmy

        create_timmy()

        kwargs = MockOllama.call_args.kwargs
        assert kwargs["host"] == custom_url


def test_create_timmy_explicit_ollama_ignores_autodetect():
    """backend='ollama' must always use Ollama, even on Apple Silicon."""
    with (
        patch("timmy.agent.Agent") as MockAgent,
        patch("timmy.agent.Ollama"),
        patch("timmy.agent.SqliteDb"),
        patch("timmy.agent._resolve_model_with_fallback", return_value=("llama3.2:3b", False)),
        patch("timmy.agent._check_model_available", return_value=True),
        patch("timmy.agent._warmup_model", return_value=True),
    ):
        from timmy.agent import create_timmy

        create_timmy(backend="ollama")

    MockAgent.assert_called_once()


# ── _resolve_backend ─────────────────────────────────────────────────────────


def test_resolve_backend_explicit_takes_priority():
    from timmy.agent import _resolve_backend

    assert _resolve_backend("ollama") == "ollama"


def test_resolve_backend_defaults_to_ollama_without_config():
    """Default config (timmy_model_backend='ollama') → 'ollama'."""
    from timmy.agent import _resolve_backend

    assert _resolve_backend(None) == "ollama"


def test_model_supports_tools_llama32_returns_false():
    """llama3.2 (3B) is too small for reliable tool calling."""
    from timmy.agent import _model_supports_tools

    assert _model_supports_tools("llama3.2") is False
    assert _model_supports_tools("llama3.2:latest") is False


def test_model_supports_tools_llama31_returns_true():
    """llama3.1 (8B+) can handle tool calling."""
    from timmy.agent import _model_supports_tools

    assert _model_supports_tools("llama3.1") is True
    assert _model_supports_tools("llama3.3") is True


def test_model_supports_tools_other_small_models():
    """Other known small models should not get tools."""
    from timmy.agent import _model_supports_tools

    assert _model_supports_tools("phi-3") is False
    assert _model_supports_tools("tinyllama") is False


def test_model_supports_tools_unknown_model_gets_tools():
    """Unknown models default to tool-capable (optimistic)."""
    from timmy.agent import _model_supports_tools

    assert _model_supports_tools("mistral") is True
    assert _model_supports_tools("qwen2.5:72b") is True


# ── Tool gating in create_timmy ──────────────────────────────────────────────


def test_create_timmy_no_tools_for_small_model():
    """Small models (llama3.2) should get no tools."""
    mock_toolkit = MagicMock()
    with (
        patch("timmy.agent.Agent") as MockAgent,
        patch("timmy.agent.Ollama"),
        patch("timmy.agent.SqliteDb"),
        patch("timmy.agent.create_full_toolkit", return_value=mock_toolkit),
        patch("timmy.agent._resolve_model_with_fallback", return_value=("llama3.2:3b", False)),
        patch("timmy.agent._check_model_available", return_value=True),
        patch("timmy.agent._warmup_model", return_value=True),
    ):
        from timmy.agent import create_timmy

        create_timmy()

        kwargs = MockAgent.call_args.kwargs
        # llama3.2 is in _SMALL_MODEL_PATTERNS → tools should be None
        assert kwargs["tools"] is None


def test_create_timmy_includes_tools_for_large_model():
    """A tool-capable model (e.g. llama3.1) should attempt to include tools."""
    mock_toolkit = MagicMock()

    with (
        patch("timmy.agent.Agent") as MockAgent,
        patch("timmy.agent.Ollama"),
        patch("timmy.agent.SqliteDb"),
        patch("timmy.agent.create_full_toolkit", return_value=mock_toolkit),
        patch("timmy.agent.settings") as mock_settings,
        patch("timmy.agent._resolve_model_with_fallback", return_value=("llama3.1", False)),
        patch("timmy.agent._check_model_available", return_value=True),
        patch("timmy.agent._warmup_model", return_value=True),
    ):
        mock_settings.ollama_model = "llama3.1"
        mock_settings.ollama_url = "http://localhost:11434"
        mock_settings.ollama_num_ctx = 4096
        mock_settings.timmy_model_backend = "ollama"
        mock_settings.telemetry_enabled = False

        from timmy.agent import create_timmy

        create_timmy()

        kwargs = MockAgent.call_args.kwargs
        assert mock_toolkit in kwargs["tools"]


def test_create_timmy_no_unsupported_agent_kwargs():
    """Regression guard: show_tool_calls and tool_call_limit are not valid agno 2.x params.

    These were removed in f95c960 (Feb 26) and must not be reintroduced.
    """
    with (
        patch("timmy.agent.Agent") as MockAgent,
        patch("timmy.agent.Ollama"),
        patch("timmy.agent.SqliteDb"),
        patch("timmy.agent._resolve_model_with_fallback", return_value=("llama3.2:3b", False)),
        patch("timmy.agent._check_model_available", return_value=True),
        patch("timmy.agent._warmup_model", return_value=True),
    ):
        from timmy.agent import create_timmy

        create_timmy()

        kwargs = MockAgent.call_args.kwargs
        assert "show_tool_calls" not in kwargs, "show_tool_calls is not a valid Agent param"


def test_create_timmy_no_extra_kwargs():
    """All kwargs passed to Agent() must be from the known-valid set.

    agno is mocked globally in conftest, so we can't inspect the real class here.
    Instead, maintain an explicit allowlist of params validated against agno 2.5.5.
    If a new param is needed, verify it exists in agno first, then add it here.
    """
    VALID_AGENT_KWARGS = {
        "name",
        "model",
        "db",
        "description",
        "add_history_to_context",
        "num_history_runs",
        "markdown",
        "tools",
        "tool_call_limit",
        "telemetry",
    }

    with (
        patch("timmy.agent.Agent") as MockAgent,
        patch("timmy.agent.Ollama"),
        patch("timmy.agent.SqliteDb"),
        patch("timmy.agent._resolve_model_with_fallback", return_value=("llama3.2:3b", False)),
        patch("timmy.agent._check_model_available", return_value=True),
        patch("timmy.agent._warmup_model", return_value=True),
    ):
        from timmy.agent import create_timmy

        create_timmy()

        kwargs = MockAgent.call_args.kwargs
        invalid = set(kwargs.keys()) - VALID_AGENT_KWARGS
        assert not invalid, (
            f"Unknown Agent kwargs {invalid} — verify they exist in agno "
            f"before adding to VALID_AGENT_KWARGS"
        )


# ── skip_mcp flag (#72) ─────────────────────────────────────────────────────


def test_create_timmy_skip_mcp_omits_mcp_tools():
    """create_timmy(skip_mcp=True) must not add MCP tool servers."""
    with (
        patch("timmy.agent.Agent"),
        patch("timmy.agent.Ollama"),
        patch("timmy.agent.SqliteDb"),
        patch("timmy.mcp_tools.create_gitea_mcp_tools") as mock_gitea_mcp,
        patch("timmy.mcp_tools.create_filesystem_mcp_tools") as mock_fs_mcp,
        patch("timmy.agent._resolve_model_with_fallback", return_value=("llama3.2:3b", False)),
        patch("timmy.agent._check_model_available", return_value=True),
        patch("timmy.agent._warmup_model", return_value=True),
    ):
        from timmy.agent import create_timmy

        create_timmy(skip_mcp=True)

        # MCP factory functions should never be called
        mock_gitea_mcp.assert_not_called()
        mock_fs_mcp.assert_not_called()


def test_create_timmy_default_includes_mcp_tools():
    """create_timmy() without skip_mcp should attempt MCP tool creation."""
    with (
        patch("timmy.agent.Agent"),
        patch("timmy.agent.Ollama"),
        patch("timmy.agent.SqliteDb"),
        patch("timmy.mcp_tools.create_gitea_mcp_tools", return_value=None) as mock_gitea_mcp,
        patch("timmy.mcp_tools.create_filesystem_mcp_tools", return_value=None) as mock_fs_mcp,
        patch("timmy.agent._resolve_model_with_fallback", return_value=("llama3.1", False)),
        patch("timmy.agent._check_model_available", return_value=True),
        patch("timmy.agent._warmup_model", return_value=True),
    ):
        from timmy.agent import create_timmy

        create_timmy(skip_mcp=False)

        # MCP factories should be called when skip_mcp is False
        mock_gitea_mcp.assert_called_once()
        mock_fs_mcp.assert_called_once()


# ── Configurable fallback chain tests ────────────────────────────────────────


def test_settings_has_fallback_model_lists():
    """settings.fallback_models and vision_fallback_models exist and are lists."""
    from config import settings

    assert isinstance(settings.fallback_models, list)
    assert isinstance(settings.vision_fallback_models, list)
    assert len(settings.fallback_models) > 0
    assert len(settings.vision_fallback_models) > 0


def test_resolve_model_uses_configurable_text_fallback():
    """_resolve_model_with_fallback walks settings.fallback_models for text models."""
    with patch("timmy.agent.settings") as mock_settings:
        mock_settings.ollama_model = "nonexistent-model"
        mock_settings.fallback_models = ["custom-a", "custom-b"]
        mock_settings.vision_fallback_models = ["vision-a"]

        # First model in chain is available
        with patch("timmy.agent._check_model_available", side_effect=lambda m: m == "custom-a"):
            from timmy.agent import _resolve_model_with_fallback

            model, is_fallback = _resolve_model_with_fallback(
                requested_model="nonexistent-model",
                require_vision=False,
                auto_pull=False,
            )
            assert model == "custom-a"
            assert is_fallback is True


def test_resolve_model_uses_configurable_vision_fallback():
    """_resolve_model_with_fallback walks settings.vision_fallback_models for vision."""
    with patch("timmy.agent.settings") as mock_settings:
        mock_settings.ollama_model = "nonexistent-model"
        mock_settings.fallback_models = ["text-a"]
        mock_settings.vision_fallback_models = ["vision-x", "vision-y"]

        with patch("timmy.agent._check_model_available", side_effect=lambda m: m == "vision-y"):
            from timmy.agent import _resolve_model_with_fallback

            model, is_fallback = _resolve_model_with_fallback(
                requested_model="nonexistent-model",
                require_vision=True,
                auto_pull=False,
            )
            assert model == "vision-y"
            assert is_fallback is True


def test_get_effective_ollama_model_walks_fallback_chain():
    """get_effective_ollama_model uses settings.fallback_models."""
    with (
        patch("config.settings") as mock_settings,
        patch("config.check_ollama_model_available", side_effect=lambda m: m == "fb-2") as _,
    ):
        mock_settings.ollama_model = "gone-model"
        mock_settings.ollama_url = "http://localhost:11434"
        mock_settings.fallback_models = ["fb-1", "fb-2", "fb-3"]

        from config import get_effective_ollama_model

        result = get_effective_ollama_model()
        assert result == "fb-2"


# ── _build_tools_list ─────────────────────────────────────────────────────


def test_build_tools_list_empty_when_tools_disabled():
    """Small models get an empty tools list."""
    from timmy.agent import _build_tools_list

    result = _build_tools_list(use_tools=False, skip_mcp=False, model_name="llama3.2")
    assert result == []


def test_build_tools_list_includes_toolkit_when_enabled():
    """Tool-capable models get the full toolkit."""
    mock_toolkit = MagicMock()
    with patch("timmy.agent.create_full_toolkit", return_value=mock_toolkit):
        from timmy.agent import _build_tools_list

        result = _build_tools_list(use_tools=True, skip_mcp=True, model_name="llama3.1")
        assert mock_toolkit in result


def test_build_tools_list_skips_mcp_when_flagged():
    """skip_mcp=True must not call MCP factories."""
    mock_toolkit = MagicMock()
    with (
        patch("timmy.agent.create_full_toolkit", return_value=mock_toolkit),
        patch("timmy.mcp_tools.create_gitea_mcp_tools") as mock_gitea,
        patch("timmy.mcp_tools.create_filesystem_mcp_tools") as mock_fs,
    ):
        from timmy.agent import _build_tools_list

        _build_tools_list(use_tools=True, skip_mcp=True, model_name="llama3.1")
        mock_gitea.assert_not_called()
        mock_fs.assert_not_called()


def test_build_tools_list_includes_mcp_when_not_skipped():
    """skip_mcp=False should attempt MCP tool creation."""
    mock_toolkit = MagicMock()
    with (
        patch("timmy.agent.create_full_toolkit", return_value=mock_toolkit),
        patch("timmy.mcp_tools.create_gitea_mcp_tools", return_value=None) as mock_gitea,
        patch("timmy.mcp_tools.create_filesystem_mcp_tools", return_value=None) as mock_fs,
    ):
        from timmy.agent import _build_tools_list

        _build_tools_list(use_tools=True, skip_mcp=False, model_name="llama3.1")
        mock_gitea.assert_called_once()
        mock_fs.assert_called_once()


# ── _build_prompt ─────────────────────────────────────────────────────────


def test_build_prompt_includes_base_prompt():
    """Prompt should always contain the base system prompt."""
    from timmy.agent import _build_prompt

    result = _build_prompt(use_tools=False, session_id="test")
    assert "Timmy" in result


def test_build_prompt_appends_memory_context():
    """Memory context should be appended when available."""
    mock_memory = MagicMock()
    mock_memory.get_system_context.return_value = "User prefers dark mode."
    with patch("timmy.memory_system.memory_system", mock_memory):
        from timmy.agent import _build_prompt

        result = _build_prompt(use_tools=True, session_id="test")
        assert "GROUNDED CONTEXT" in result
        assert "dark mode" in result


def test_build_prompt_truncates_long_memory():
    """Long memory context should be truncated."""
    mock_memory = MagicMock()
    mock_memory.get_system_context.return_value = "x" * 10000
    with patch("timmy.memory_system.memory_system", mock_memory):
        from timmy.agent import _build_prompt

        result = _build_prompt(use_tools=False, session_id="test")
        assert "[truncated]" in result


def test_build_prompt_survives_memory_failure():
    """Prompt should fall back to base when memory fails."""
    mock_memory = MagicMock()
    mock_memory.get_system_context.side_effect = RuntimeError("db locked")
    with patch("timmy.memory_system.memory_system", mock_memory):
        from timmy.agent import _build_prompt

        result = _build_prompt(use_tools=True, session_id="test")
        assert "Timmy" in result
        # Memory context should NOT be appended (the db locked error was caught)
        assert "db locked" not in result


# ── _create_ollama_agent ──────────────────────────────────────────────────


def test_create_ollama_agent_passes_correct_kwargs():
    """_create_ollama_agent must pass the expected kwargs to Agent."""
    with (
        patch("timmy.agent.Agent") as MockAgent,
        patch("timmy.agent.Ollama"),
        patch("timmy.agent.SqliteDb"),
        patch("timmy.agent._warmup_model", return_value=True),
    ):
        from timmy.agent import _create_ollama_agent

        _create_ollama_agent(
            db_file="test.db",
            model_name="llama3.1",
            tools_list=[MagicMock()],
            full_prompt="test prompt",
            use_tools=True,
        )
        kwargs = MockAgent.call_args.kwargs
        assert kwargs["description"] == "test prompt"
        assert kwargs["markdown"] is False


def test_create_ollama_agent_none_tools_when_empty():
    """Empty tools_list should pass tools=None to Agent."""
    with (
        patch("timmy.agent.Agent") as MockAgent,
        patch("timmy.agent.Ollama"),
        patch("timmy.agent.SqliteDb"),
        patch("timmy.agent._warmup_model", return_value=True),
    ):
        from timmy.agent import _create_ollama_agent

        _create_ollama_agent(
            db_file="test.db",
            model_name="llama3.2",
            tools_list=[],
            full_prompt="test prompt",
            use_tools=False,
        )
        kwargs = MockAgent.call_args.kwargs
        assert kwargs["tools"] is None


def test_no_hardcoded_fallback_constants_in_agent():
    """agent.py must not define module-level DEFAULT_MODEL_FALLBACKS."""
    import timmy.agent as agent_mod

    assert not hasattr(agent_mod, "DEFAULT_MODEL_FALLBACKS"), (
        "Hardcoded DEFAULT_MODEL_FALLBACKS still exists — use settings.fallback_models"
    )
    assert not hasattr(agent_mod, "VISION_MODEL_FALLBACKS"), (
        "Hardcoded VISION_MODEL_FALLBACKS still exists — use settings.vision_fallback_models"
    )
