from __future__ import annotations

from pathlib import Path
from unittest import mock

import pytest

from dspy_file import analyze_file_cli


def test_probe_openai_provider_success() -> None:
    mock_response = mock.MagicMock()
    urlopen_mock = mock.MagicMock()
    urlopen_mock.return_value.__enter__.return_value = mock_response

    with mock.patch(
        "dspy_file.analyze_file_cli.request.urlopen", urlopen_mock
    ):
        analyze_file_cli._probe_openai_provider("http://localhost:1234/v1", "token")

    urlopen_mock.assert_called_once()


def test_probe_openai_provider_raises_on_failure() -> None:
    with mock.patch(
        "dspy_file.analyze_file_cli.request.urlopen",
        side_effect=analyze_file_cli.urlerror.URLError("connection refused"),
    ):
        with pytest.raises(analyze_file_cli.ProviderConnectivityError):
            analyze_file_cli._probe_openai_provider("http://localhost:1234/v1", "token")


def test_main_exits_early_when_lmstudio_unreachable(tmp_path: Path) -> None:
    source = tmp_path / "example.md"
    source.write_text("content", encoding="utf-8")

    with mock.patch.object(
        analyze_file_cli,
        "_probe_openai_provider",
        side_effect=analyze_file_cli.ProviderConnectivityError("unreachable"),
    ), mock.patch.object(analyze_file_cli, "configure_model") as configure_mock:
        exit_code = analyze_file_cli.main(
            ["--provider", "lmstudio", str(source)]
        )

    assert exit_code == 1
    configure_mock.assert_not_called()


def test_configure_model_disables_json_fallback_for_lmstudio() -> None:
    with mock.patch.object(analyze_file_cli.dspy, "LM") as lm_mock, mock.patch.object(
        analyze_file_cli.dspy, "ChatAdapter"
    ) as chat_adapter_mock, mock.patch.object(
        analyze_file_cli.dspy, "configure"
    ) as configure_mock:
        lm_instance = object()
        adapter_instance = object()
        lm_mock.return_value = lm_instance
        chat_adapter_mock.return_value = adapter_instance

        analyze_file_cli.configure_model(
            analyze_file_cli.Provider.LMSTUDIO,
            "test-model",
            api_base="http://localhost:1234/v1",
            api_key="lm-studio",
        )

    lm_mock.assert_called_once()
    chat_adapter_mock.assert_called_once_with(
        use_native_function_calling=False,
        use_json_adapter_fallback=False,
    )
    configure_mock.assert_called_once_with(lm=lm_instance, adapter=adapter_instance)


def test_select_lmstudio_instance_ids_matches_loaded_instance_variants() -> None:
    payload = {
        "models": [
            {
                "key": "qwen_qwen3-4b-instruct-2507",
                "display_name": "Qwen 3 4B Instruct 2507",
                "loaded_instances": [
                    {"id": "qwen_qwen3-4b-instruct-2507"},
                    {"id": "openai/qwen_qwen3-4b-instruct-2507"},
                ],
            }
        ]
    }

    result = analyze_file_cli._select_lmstudio_instance_ids(
        payload,
        "qwen_qwen3-4b-instruct-2507",
    )

    assert result == [
        "qwen_qwen3-4b-instruct-2507",
        "openai/qwen_qwen3-4b-instruct-2507",
    ]


def test_stop_lmstudio_model_unloads_matching_instances() -> None:
    models_payload = {
        "models": [
            {
                "key": "qwen_qwen3-4b-instruct-2507",
                "display_name": "Qwen 3 4B Instruct 2507",
                "loaded_instances": [{"id": "qwen_qwen3-4b-instruct-2507"}],
            }
        ]
    }

    list_response = mock.MagicMock()
    list_response.read.return_value = __import__("json").dumps(models_payload).encode("utf-8")
    unload_response = mock.MagicMock()

    def fake_urlopen(req: object, timeout: float = 0.0):
        method = req.get_method()
        cm = mock.MagicMock()
        if method == "GET":
            cm.__enter__.return_value = list_response
        else:
            cm.__enter__.return_value = unload_response
        return cm

    with mock.patch("dspy_file.analyze_file_cli.request.urlopen", side_effect=fake_urlopen) as urlopen_mock:
        analyze_file_cli.stop_lmstudio_model(
            "qwen_qwen3-4b-instruct-2507",
            api_base="http://localhost:1234/v1",
            api_key="lm-studio",
        )

    assert urlopen_mock.call_count == 2
    list_request = urlopen_mock.call_args_list[0].args[0]
    unload_request = urlopen_mock.call_args_list[1].args[0]
    assert list_request.full_url == "http://localhost:1234/api/v1/models"
    assert unload_request.full_url == "http://localhost:1234/api/v1/models/unload"
    assert unload_request.data == b'{"instance_id": "qwen_qwen3-4b-instruct-2507"}'


def test_main_stops_lmstudio_model_when_run_completes(tmp_path: Path) -> None:
    source = tmp_path / "example.md"
    source.write_text("# Title\n\nBody", encoding="utf-8")
    model_name = "qwen_qwen3-4b-instruct-2507"

    with mock.patch.object(analyze_file_cli, "_probe_openai_provider"), mock.patch.object(
        analyze_file_cli, "configure_model"
    ), mock.patch.object(
        analyze_file_cli, "analyze_path", return_value=0
    ), mock.patch.object(
        analyze_file_cli, "stop_lmstudio_model"
    ) as stop_mock:
        exit_code = analyze_file_cli.main(
            ["--provider", "lmstudio", "--model", model_name, str(source)]
        )

    assert exit_code == 0
    stop_mock.assert_called_once_with(
        model_name,
        api_base=analyze_file_cli.LMSTUDIO_BASE_URL,
        api_key="lm-studio",
    )
