"""Tests for llmclient module.

Tests cover configuration management, worker threads, GUI components,
and integration with LLM server communication.
"""

from __future__ import annotations

import json
import sys
from pathlib import Path
from unittest.mock import MagicMock, Mock, patch

import pytest

from pytola.llm.llmclient.gui import (
    CONNECTION_TIMEOUT,
    ConnectionTestWorker,
    Language,
    LLMChatApp,
    LLMClientConfig,
    LLMWorker,
)

# Check if PySide2 and pytest-qt are available
try:
    from PySide2.QtCore import QThread  # noqa: F401
    from PySide2.QtWidgets import QApplication  # noqa: F401

    GUI_AVAILABLE = True
except ImportError:
    GUI_AVAILABLE = False

try:
    import pytestqt  # noqa: F401

    PYTEST_QT_AVAILABLE = True
except ImportError:
    PYTEST_QT_AVAILABLE = False

# Disable GUI tests on Windows due to compatibility issues
SKIP_GUI_TESTS = sys.platform == "win32"
GUI_TESTS_ENABLED = GUI_AVAILABLE and PYTEST_QT_AVAILABLE and not SKIP_GUI_TESTS


@pytest.fixture
def temp_config_file(tmp_path: Path) -> Path:
    """Create a temporary configuration file."""
    return tmp_path / "llmclient.json"


@pytest.fixture
def sample_config_data() -> dict:
    """Sample configuration data for testing."""
    return {
        "TITLE": "Test LLM Client",
        "WIN_SIZE": [1024, 768],
        "WIN_POS": [50, 50],
        "SERVER_URL": "http://test.server:9000",
        "MAX_TOKENS": 512,
        "TEMPERATURE": 0.8,
        "TOP_P": 0.95,
        "TOP_K": 50,
    }


class TestLLMClientConfig:
    """Tests for LLMClientConfig configuration management."""

    def test_default_configuration_values(self, tmp_path: Path):
        """Test loading default configuration when no config file exists."""
        with patch("pytola.llm.llmclient.gui.CONFIG_FILE", tmp_path / "nonexistent.json"):
            config = LLMClientConfig()

            assert config.TITLE == "LLM Chat Client v1.0"
            assert config.WIN_SIZE == [900, 700]
            assert config.WIN_POS == [100, 100]
            assert config.SERVER_URL == "http://localhost:8080"
            assert config.MAX_TOKENS == 256
            assert config.TEMPERATURE == 0.7
            assert config.TOP_P == 0.9
            assert config.TOP_K == 40

    def test_load_configuration_from_file(self, temp_config_file: Path, sample_config_data: dict):
        """Test loading configuration from existing JSON file."""
        temp_config_file.write_text(json.dumps(sample_config_data))

        with patch("pytola.llm.llmclient.gui.CONFIG_FILE", temp_config_file):
            config = LLMClientConfig()

            assert config.TITLE == "Test LLM Client"
            assert config.WIN_SIZE == [1024, 768]
            assert config.WIN_POS == [50, 50]
            assert config.SERVER_URL == "http://test.server:9000"
            assert config.MAX_TOKENS == 512
            assert config.TEMPERATURE == 0.8
            assert config.TOP_P == 0.95
            assert config.TOP_K == 50

    def test_save_configuration_to_file(self, temp_config_file: Path):
        """Test saving configuration to JSON file."""
        with patch("pytola.llm.llmclient.gui.CONFIG_FILE", temp_config_file):
            config = LLMClientConfig()
            config.SERVER_URL = "http://custom.server:8081"
            config.MAX_TOKENS = 1024
            config.save()

            assert temp_config_file.exists()
            saved_data = json.loads(temp_config_file.read_text())
            assert saved_data["SERVER_URL"] == "http://custom.server:8081"
            assert saved_data["MAX_TOKENS"] == 1024

    def test_configuration_handles_invalid_json(self, temp_config_file: Path):
        """Test configuration handles corrupted JSON file gracefully."""
        temp_config_file.write_text("invalid json content{{{")

        with patch("pytola.llm.llmclient.gui.CONFIG_FILE", temp_config_file):
            config = LLMClientConfig()

            # Should fall back to defaults
            assert config.SERVER_URL == "http://localhost:8080"
            assert config.MAX_TOKENS == 256

    def test_configuration_handles_invalid_data_types(self, temp_config_file: Path):
        """Test configuration handles invalid data types in JSON."""
        invalid_data = {
            "TITLE": 12345,  # Should be string
            "MAX_TOKENS": "not_a_number",  # Should be int
        }
        temp_config_file.write_text(json.dumps(invalid_data))

        with patch("pytola.llm.llmclient.gui.CONFIG_FILE", temp_config_file):
            config = LLMClientConfig()

            # Invalid data should be loaded but may cause issues
            assert hasattr(config, "TITLE")

    def test_configuration_creates_parent_directory(self, tmp_path: Path):
        """Test that save creates parent directory if it doesn't exist."""
        nested_path = tmp_path / "deep" / "nested" / "config.json"

        with patch("pytola.llm.llmclient.gui.CONFIG_FILE", nested_path):
            config = LLMClientConfig()
            config.save()

            assert nested_path.parent.exists()
            assert nested_path.exists()

    def test_configuration_range_constants(self):
        """Test that configuration range constants are correctly defined."""
        config = LLMClientConfig()

        assert config.MAX_TOKENS_RANGE == [1, 4096]
        assert config.TEMPERATURE_RANGE == [0.0, 2.0]
        assert config.TOP_P_RANGE == [0.0, 1.0]
        assert config.TOP_K_RANGE == [1, 100]


class TestLLMWorker:
    """Tests for LLMWorker thread functionality."""

    def test_worker_initialization(self):
        """Test LLMWorker thread initialization with parameters."""
        worker = LLMWorker(
            prompt="Test prompt",
            server_url="http://localhost:8080",
            max_tokens=256,
            temperature=0.7,
            top_p=0.9,
            top_k=40,
        )

        assert worker.prompt == "Test prompt"
        assert worker.server_url == "http://localhost:8080"
        assert worker.max_tokens == 256
        assert worker.temperature == 0.7
        assert worker.top_p == 0.9
        assert worker.top_k == 40
        assert worker._is_running is True

    def test_worker_stop_method(self):
        """Test stopping worker thread execution."""
        worker = LLMWorker(
            prompt="Test",
            server_url="http://localhost:8080",
            max_tokens=100,
            temperature=0.5,
            top_p=0.8,
            top_k=30,
        )

        assert worker._is_running is True
        worker.stop()
        assert worker._is_running is False

    @patch("pytola.llm.llmclient.gui.urlopen")
    def test_worker_handles_http_error_response(self, mock_urlopen):
        """Test worker handles non-200 HTTP response correctly."""
        mock_response = MagicMock()
        mock_response.status = 500
        mock_response.read.return_value = b"Internal Server Error"
        mock_response.__enter__ = Mock(return_value=mock_response)
        mock_response.__exit__ = Mock(return_value=False)
        mock_urlopen.return_value = mock_response

        worker = LLMWorker(
            prompt="Test",
            server_url="http://localhost:8080",
            max_tokens=100,
            temperature=0.5,
            top_p=0.8,
            top_k=30,
        )

        error_emitted = []
        worker.error_occurred.connect(lambda msg: error_emitted.append(msg))

        worker.run()

        assert len(error_emitted) == 1
        assert "500" in error_emitted[0]

    @patch("pytola.llm.llmclient.gui.urlopen")
    def test_worker_processes_streaming_response(self, mock_urlopen):
        """Test worker processes SSE streaming response correctly."""
        # Simulate SSE response
        mock_response = MagicMock()
        mock_response.status = 200
        mock_response.__iter__ = Mock(
            return_value=iter(
                [
                    b'data: {"content": "Hello"}\n',
                    b'data: {"content": " World"}\n',
                ]
            )
        )
        mock_response.__enter__ = Mock(return_value=mock_response)
        mock_response.__exit__ = Mock(return_value=False)
        mock_urlopen.return_value = mock_response

        worker = LLMWorker(
            prompt="Test",
            server_url="http://localhost:8080",
            max_tokens=100,
            temperature=0.5,
            top_p=0.8,
            top_k=30,
        )

        responses = []
        worker.response_received.connect(lambda text: responses.append(text))

        worker.run()

        assert len(responses) > 0
        assert "Hello" in responses[-1]

    @patch("pytola.llm.llmclient.gui.urlopen")
    def test_worker_handles_connection_error(self, mock_urlopen):
        """Test worker handles URLError connection failures."""
        from urllib.error import URLError

        mock_urlopen.side_effect = URLError("Connection refused")

        worker = LLMWorker(
            prompt="Test",
            server_url="http://localhost:8080",
            max_tokens=100,
            temperature=0.5,
            top_p=0.8,
            top_k=30,
        )

        error_emitted = []
        worker.error_occurred.connect(lambda msg: error_emitted.append(msg))

        worker.run()

        assert len(error_emitted) == 1
        assert "Connection error" in error_emitted[0]

    @patch("pytola.llm.llmclient.gui.urlopen")
    def test_worker_handles_malformed_json(self, mock_urlopen):
        """Test worker handles malformed JSON in SSE stream gracefully."""
        mock_response = MagicMock()
        mock_response.status = 200
        mock_response.__iter__ = Mock(
            return_value=iter(
                [
                    b"data: {invalid json}\n",
                    b'data: {"content": "Valid"}\n',
                ]
            )
        )
        mock_response.__enter__ = Mock(return_value=mock_response)
        mock_response.__exit__ = Mock(return_value=False)
        mock_urlopen.return_value = mock_response

        worker = LLMWorker(
            prompt="Test",
            server_url="http://localhost:8080",
            max_tokens=100,
            temperature=0.5,
            top_p=0.8,
            top_k=30,
        )

        responses = []
        worker.response_received.connect(lambda text: responses.append(text))

        worker.run()

        # Should continue processing after invalid JSON
        assert len(responses) > 0
        assert "Valid" in responses[-1]


class TestConnectionTestWorker:
    """Tests for ConnectionTestWorker functionality."""

    def test_connection_worker_initialization(self):
        """Test ConnectionTestWorker initialization."""
        worker = ConnectionTestWorker("http://localhost:8080")

        assert worker.server_url == "http://localhost:8080"

    @patch("pytola.llm.llmclient.gui.urlopen")
    def test_connection_success(self, mock_urlopen):
        """Test successful connection to server health endpoint."""
        mock_response = MagicMock()
        mock_response.status = 200
        mock_response.__enter__ = Mock(return_value=mock_response)
        mock_response.__exit__ = Mock(return_value=False)
        mock_urlopen.return_value = mock_response

        worker = ConnectionTestWorker("http://localhost:8080")

        results = []
        worker.result_ready.connect(lambda success, msg: results.append((success, msg)))

        worker.run()

        assert len(results) == 1
        assert results[0][0] is True
        assert "successful" in results[0][1].lower()

    @patch("pytola.llm.llmclient.gui.urlopen")
    def test_connection_failure_non_200_status(self, mock_urlopen):
        """Test connection failure with non-200 HTTP status."""
        mock_response = MagicMock()
        mock_response.status = 503
        mock_response.__enter__ = Mock(return_value=mock_response)
        mock_response.__exit__ = Mock(return_value=False)
        mock_urlopen.return_value = mock_response

        worker = ConnectionTestWorker("http://localhost:8080")

        results = []
        worker.result_ready.connect(lambda success, msg: results.append((success, msg)))

        worker.run()

        assert len(results) == 1
        assert results[0][0] is False
        assert "503" in results[0][1]

    @patch("pytola.llm.llmclient.gui.urlopen")
    def test_connection_handles_url_error(self, mock_urlopen):
        """Test connection handles URLError gracefully."""
        from urllib.error import URLError

        mock_urlopen.side_effect = URLError("Connection refused")

        worker = ConnectionTestWorker("http://localhost:8080")

        results = []
        worker.result_ready.connect(lambda success, msg: results.append((success, msg)))

        worker.run()

        assert len(results) == 1
        assert results[0][0] is False
        assert "Connection error" in results[0][1]

    def test_connection_timeout_constant(self):
        """Test that connection timeout constant is defined."""
        assert CONNECTION_TIMEOUT == 5


@pytest.mark.skipif(
    not GUI_TESTS_ENABLED,
    reason="GUI tests disabled on Windows due to pytest-qt compatibility issues (DLL loading error 0xc0000139)",
)
class TestLLMChatApp:
    """Tests for LLMChatApp GUI functionality."""

    def test_app_initialization(self, qtbot, tmp_path: Path):
        """Test LLMChatApp window initialization."""
        with patch("pytola.llm.llmclient.gui.CONFIG_FILE", tmp_path / "test_config.json"):
            window = LLMChatApp()
            qtbot.addWidget(window)

            assert window.windowTitle() == "LLM Chat Client v1.0"
            assert window.worker_thread is None
            assert window.test_thread is None
            assert window.current_ai_start_pos == -1

    def test_ui_components_created(self, qtbot, tmp_path: Path):
        """Test that all UI components are created properly."""
        with patch("pytola.llm.llmclient.gui.CONFIG_FILE", tmp_path / "test_config.json"):
            window = LLMChatApp()
            qtbot.addWidget(window)

            # Check widgets exist
            assert hasattr(window, "server_url_input")
            assert hasattr(window, "test_connection_btn")
            assert hasattr(window, "max_tokens_spin")
            assert hasattr(window, "temperature_spin")
            assert hasattr(window, "top_p_spin")
            assert hasattr(window, "top_k_spin")
            assert hasattr(window, "chat_display")
            assert hasattr(window, "user_input")
            assert hasattr(window, "send_btn")
            assert hasattr(window, "stop_btn")

    def test_config_changed_updates_configuration(self, qtbot, tmp_path: Path):
        """Test that changing UI values updates configuration."""
        with patch("pytola.llm.llmclient.gui.CONFIG_FILE", tmp_path / "test_config.json"):
            from pytola.llmclient.llmclient import conf

            window = LLMChatApp()
            qtbot.addWidget(window)

            # Change server URL
            window.server_url_input.setText("http://new.server:9000")
            window.on_config_changed()
            assert conf.SERVER_URL == "http://new.server:9000"

            # Change max tokens
            window.max_tokens_spin.setValue(512)
            window.on_config_changed()
            assert conf.MAX_TOKENS == 512

    def test_send_prompt_with_empty_input(self, qtbot, tmp_path: Path):
        """Test that sending empty prompt shows error message."""
        with patch("pytola.llm.llmclient.gui.CONFIG_FILE", tmp_path / "test_config.json"):
            window = LLMChatApp()
            qtbot.addWidget(window)

            window.user_input.setText("")
            window.send_prompt()

            # Should show error in status bar
            assert "Please enter prompt" in window.statusBar().currentMessage()

    def test_send_prompt_creates_worker_thread(self, qtbot, tmp_path: Path):
        """Test that sending prompt creates worker thread."""
        with patch("pytola.llm.llmclient.gui.CONFIG_FILE", tmp_path / "test_config.json"):
            window = LLMChatApp()
            qtbot.addWidget(window)

            window.user_input.setText("Test prompt")
            window.server_url_input.setText("http://localhost:8080")

            with patch.object(LLMWorker, "start"):
                window.send_prompt()

                assert window.worker_thread is not None
                assert isinstance(window.worker_thread, LLMWorker)

    def test_stop_generation(self, qtbot, tmp_path: Path):
        """Test stopping generation calls worker stop method."""
        with patch("pytola.llm.llmclient.gui.CONFIG_FILE", tmp_path / "test_config.json"):
            window = LLMChatApp()
            qtbot.addWidget(window)

            # Create mock worker
            mock_worker = MagicMock()
            mock_worker.isRunning.return_value = True
            window.worker_thread = mock_worker

            window.stop_generation()

            mock_worker.stop.assert_called_once()

    def test_test_connection_with_empty_url(self, qtbot, tmp_path: Path):
        """Test connection test with empty server URL."""
        with patch("pytola.llm.llmclient.gui.CONFIG_FILE", tmp_path / "test_config.json"):
            window = LLMChatApp()
            qtbot.addWidget(window)

            window.server_url_input.setText("")
            window.test_connection()

            assert "Please enter server address" in window.statusBar().currentMessage()

    def test_window_move_updates_config(self, qtbot, tmp_path: Path):
        """Test that moving window updates configuration."""
        with patch("pytola.llm.llmclient.gui.CONFIG_FILE", tmp_path / "test_config.json"):
            from pytola.llmclient.llmclient import conf

            window = LLMChatApp()
            qtbot.addWidget(window)

            # Simulate move event
            window.move(200, 300)

            # Configuration should be updated
            assert conf.WIN_POS[0] >= 0
            assert conf.WIN_POS[1] >= 0

    def test_window_resize_updates_config(self, qtbot, tmp_path: Path):
        """Test that resizing window updates configuration."""
        with patch("pytola.llm.llmclient.gui.CONFIG_FILE", tmp_path / "test_config.json"):
            from pytola.llmclient.llmclient import conf

            window = LLMChatApp()
            qtbot.addWidget(window)

            # Simulate resize event
            window.resize(1024, 768)

            # Configuration should be updated
            assert conf.WIN_SIZE[0] > 0
            assert conf.WIN_SIZE[1] > 0

    def test_update_response_creates_ai_start_position(self, qtbot, tmp_path: Path):
        """Test that first response update sets AI start position."""
        with patch("pytola.llm.llmclient.gui.CONFIG_FILE", tmp_path / "test_config.json"):
            window = LLMChatApp()
            qtbot.addWidget(window)

            assert window.current_ai_start_pos == -1

            window.update_response("Test response")

            assert window.current_ai_start_pos >= 0

    def test_append_to_chat_adds_text(self, qtbot, tmp_path: Path):
        """Test appending text to chat display."""
        with patch("pytola.llm.llmclient.gui.CONFIG_FILE", tmp_path / "test_config.json"):
            window = LLMChatApp()
            qtbot.addWidget(window)

            initial_text = window.chat_display.toPlainText()
            window.append_to_chat("Test message", is_user=True)

            assert "Test message" in window.chat_display.toPlainText()
            assert len(window.chat_display.toPlainText()) > len(initial_text)

    def test_handle_error_shows_error_message(self, qtbot, tmp_path: Path):
        """Test error handling displays error in chat and status."""
        with patch("pytola.llm.llmclient.gui.CONFIG_FILE", tmp_path / "test_config.json"):
            window = LLMChatApp()
            qtbot.addWidget(window)

            window.handle_error("Test error message")

            assert "Test error message" in window.chat_display.toPlainText()
            assert "Test error message" in window.statusBar().currentMessage()


class TestIntegration:
    """Integration tests for complete workflows."""

    @pytest.mark.skipif(
        not GUI_TESTS_ENABLED,
        reason="GUI tests disabled on Windows due to pytest-qt compatibility issues (DLL loading error 0xc0000139)",
    )
    def test_complete_config_save_load_cycle(self, qtbot, tmp_path: Path):
        """Test complete configuration save and load cycle."""
        config_file = tmp_path / "integration_config.json"

        # Create window and modify config
        with patch("pytola.llm.llmclient.gui.CONFIG_FILE", config_file):
            from pytola.llmclient.llmclient import conf

            window = LLMChatApp()
            qtbot.addWidget(window)

            # Modify configuration
            window.server_url_input.setText("http://integration.test:9999")
            window.max_tokens_spin.setValue(2048)
            window.temperature_spin.setValue(1.2)
            window.on_config_changed()

            # Save configuration
            conf.save()

            assert config_file.exists()

        # Load configuration in new instance
        with patch("pytola.llm.llmclient.gui.CONFIG_FILE", config_file):
            new_config = LLMClientConfig()

            assert new_config.SERVER_URL == "http://integration.test:9999"
            assert new_config.MAX_TOKENS == 2048
            assert new_config.TEMPERATURE == 1.2

    def test_config_persistence_with_atexit(self, tmp_path: Path):
        """Test that configuration can be saved manually (simulating atexit behavior)."""
        config_file = tmp_path / "atexit_config.json"

        with patch("pytola.llm.llmclient.gui.CONFIG_FILE", config_file):
            from pytola.llm.llmclient.gui import conf

            # Modify configuration
            conf.SERVER_URL = "http://atexit.test:7777"
            conf.MAX_TOKENS = 1024

            # Manually save (simulating what atexit.register would do)
            conf.save()

            assert config_file.exists()
            saved_data = json.loads(config_file.read_text())
            assert saved_data["SERVER_URL"] == "http://atexit.test:7777"
            assert saved_data["MAX_TOKENS"] == 1024

    def test_translation_manager_default_language(self):
        """Test that TranslationManager has correct default language."""
        from pytola.llm.llmclient.gui import _translation_manager

        assert _translation_manager.current_language.value == "zh"

    def test_translation_manager_switch_language(self):
        """Test switching between languages."""
        from pytola.llm.llmclient.gui import Language, _translation_manager

        # Test switching to English
        _translation_manager.set_language(Language.ENGLISH)
        assert _translation_manager.current_language == Language.ENGLISH

        # Test switching to Chinese
        _translation_manager.set_language(Language.CHINESE)
        assert _translation_manager.current_language == Language.CHINESE

    def test_translation_manager_translate(self):
        """Test translation functionality."""
        from pytola.llm.llmclient.gui import _translation_manager

        # Test Chinese translation
        _translation_manager.set_language(Language.CHINESE)
        assert "服务器" in _translation_manager.tr("server_settings")

        # Test English translation
        _translation_manager.set_language(Language.ENGLISH)
        assert "Server" in _translation_manager.tr("server_settings")

    def test_worker_signal_definition(self):
        """Test that LLMWorker signals are properly defined."""
        from pytola.llm.llmclient.gui import LLMWorker

        # Check that signals exist
        assert hasattr(LLMWorker, "response_received")
        assert hasattr(LLMWorker, "error_occurred")
        assert hasattr(LLMWorker, "is_finished")

    def test_connection_worker_signal_definition(self):
        """Test that ConnectionTestWorker signals are properly defined."""
        from pytola.llm.llmclient.gui import ConnectionTestWorker

        # Check that signal exists
        assert hasattr(ConnectionTestWorker, "result_ready")

    def test_config_constants(self):
        """Test that configuration constants are correctly defined."""
        config = LLMClientConfig()

        # Check all class variables (constants)
        assert config.WIN_SIZE == [900, 700]
        assert config.WIN_POS == [100, 100]
        assert isinstance(config.SERVER_URL, str)
        assert isinstance(config.MAX_TOKENS, int)
        assert isinstance(config.TEMPERATURE, float)
        assert isinstance(config.TOP_P, float)
        assert isinstance(config.TOP_K, int)

    def test_config_default_values_in_range(self):
        """Test that default configuration values are within allowed ranges."""
        config = LLMClientConfig()

        # Check that values are within ranges
        assert config.MAX_TOKENS_RANGE[0] <= config.MAX_TOKENS <= config.MAX_TOKENS_RANGE[1]
        assert config.TEMPERATURE_RANGE[0] <= config.TEMPERATURE <= config.TEMPERATURE_RANGE[1]
        assert config.TOP_P_RANGE[0] <= config.TOP_P <= config.TOP_P_RANGE[1]
        assert config.TOP_K_RANGE[0] <= config.TOP_K <= config.TOP_K_RANGE[1]

    def test_worker_attribute_types(self):
        """Test that LLMWorker has correct attribute types."""
        worker = LLMWorker(
            prompt="Test",
            server_url="http://localhost:8080",
            max_tokens=256,
            temperature=0.7,
            top_p=0.9,
            top_k=40,
        )

        # Check attribute types
        assert isinstance(worker.prompt, str)
        assert isinstance(worker.server_url, str)
        assert isinstance(worker.max_tokens, int)
        assert isinstance(worker.temperature, float)
        assert isinstance(worker.top_p, float)
        assert isinstance(worker.top_k, int)

    def test_connection_worker_attribute_types(self):
        """Test that ConnectionTestWorker has correct attribute types."""
        worker = ConnectionTestWorker("http://localhost:8080")

        # Check attribute type
        assert isinstance(worker.server_url, str)

    def test_connection_timeout_value(self):
        """Test that CONNECTION_TIMEOUT is set to a reasonable value."""
        assert CONNECTION_TIMEOUT > 0
        assert CONNECTION_TIMEOUT <= 60  # Should be reasonable (< 1 minute)

    def test_config_save_creates_directory(self, tmp_path: Path):
        """Test that config.save() creates parent directory if needed."""
        config_file = tmp_path / "deep" / "nested" / "config.json"

        with patch("pytola.llm.llmclient.gui.CONFIG_FILE", config_file):
            config = LLMClientConfig()
            config.save()

            assert config_file.exists()
            assert config_file.parent.exists()

    def test_config_handles_empty_file(self, tmp_path: Path):
        """Test that config loads defaults when file is empty."""
        empty_file = tmp_path / "empty.json"
        empty_file.write_text("")

        with patch("pytola.llm.llmclient.gui.CONFIG_FILE", empty_file):
            config = LLMClientConfig()

            # Should use defaults
            assert config.SERVER_URL == "http://localhost:8080"
            assert config.MAX_TOKENS == 256

    def test_config_loads_partial_data(self, tmp_path: Path):
        """Test that config loads correctly when only some fields are in file."""
        partial_file = tmp_path / "partial.json"
        partial_data = {"SERVER_URL": "http://partial.test:9000", "MAX_TOKENS": 512}
        partial_file.write_text(json.dumps(partial_data))

        with patch("pytola.llm.llmclient.gui.CONFIG_FILE", partial_file):
            config = LLMClientConfig()

            # Should load partial data
            assert config.SERVER_URL == "http://partial.test:9000"
            assert config.MAX_TOKENS == 512
            # Should use defaults for missing fields
            assert config.TEMPERATURE == 0.7

    def test_translation_manager_get_available_languages(self):
        """Test that get_available_languages returns correct list."""
        from pytola.llm.llmclient.gui import _translation_manager

        languages = _translation_manager.get_available_languages()

        assert len(languages) == 2
        assert languages[0] == ("zh", "中文")
        assert languages[1] == ("en", "English")
