from __future__ import annotations

import json
import tempfile
from pathlib import Path
from unittest.mock import patch

import pytest
from pytestqt.qtbot import QtBot

from pytola.llm.llmserver.gui import (
    LlamaServerGUI,
    LLMServerConfig,
)


class TestLLMServerConfig:
    """Tests for LLMServerConfig dataclass."""

    def test_default_configuration(self) -> None:
        """Test default configuration values."""
        with tempfile.TemporaryDirectory() as temp_dir:
            # Mock the CONFIG_FILE path to use temporary directory
            temp_config_file = Path(temp_dir) / "llmserver.json"

            # Patch the global CONFIG_FILE
            import pytola.llm.llmserver.gui as llmserver_module

            original_config_file = llmserver_module.CONFIG_FILE
            llmserver_module.CONFIG_FILE = temp_config_file

            try:
                config = LLMServerConfig()
                assert config.TITLE == "Llama Local Model Server"
                assert config.WIN_SIZE == [600, 500]
                assert config.WIN_POS == [200, 200]
                assert config.MODEL_PATH == ""
                assert config.URL == "http://127.0.0.1"
                assert config.LISTEN_PORT == 8080
                assert config.LISTEN_PORT_RNG == [1024, 65535]
                assert config.THREAD_COUNT_RNG == [1, 24]
                assert config.THREAD_COUNT == 4
            finally:
                # Restore original CONFIG_FILE
                llmserver_module.CONFIG_FILE = original_config_file
        assert config.THREAD_COUNT_RNG == [1, 24]
        assert config.THREAD_COUNT == 4

    def test_custom_initialization(self) -> None:
        """Test custom configuration initialization."""
        config = LLMServerConfig(
            MODEL_PATH="/path/to/model.gguf",
            LISTEN_PORT=9000,
            THREAD_COUNT=8,
        )
        assert config.MODEL_PATH == "/path/to/model.gguf"
        assert config.LISTEN_PORT == 9000
        assert config.THREAD_COUNT == 8

    def test_config_save_and_load(self) -> None:
        """Test configuration saving and loading functionality."""
        with tempfile.TemporaryDirectory() as temp_dir:
            # Mock the CONFIG_FILE path to use temporary directory
            temp_config_file = Path(temp_dir) / "llmserver.json"

            # Patch the global CONFIG_FILE
            import pytola.llm.llmserver.gui as llmserver_module

            original_config_file = llmserver_module.CONFIG_FILE
            llmserver_module.CONFIG_FILE = temp_config_file

            try:
                # Create and save configuration
                config = LLMServerConfig(
                    MODEL_PATH="/test/model.gguf",
                    LISTEN_PORT=8000,
                    THREAD_COUNT=6,
                )
                config.save()

                # Verify file was created
                assert temp_config_file.exists()

                # Load configuration from file
                loaded_config = LLMServerConfig()

                # Verify loaded values match saved values
                assert loaded_config.MODEL_PATH == "/test/model.gguf"
                assert loaded_config.LISTEN_PORT == 8000
                assert loaded_config.THREAD_COUNT == 6
            finally:
                # Restore original CONFIG_FILE
                llmserver_module.CONFIG_FILE = original_config_file

    def test_config_partial_load(self) -> None:
        """Test loading configuration with partial data."""
        with tempfile.TemporaryDirectory() as temp_dir:
            temp_config_file = Path(temp_dir) / "llmserver.json"

            # Create partial configuration file
            partial_data = {"MODEL_PATH": "/partial/model.gguf", "LISTEN_PORT": 7000}
            temp_config_file.write_text(json.dumps(partial_data))

            # Patch the global CONFIG_FILE
            import pytola.llm.llmserver.gui as llmserver_module

            original_config_file = llmserver_module.CONFIG_FILE
            llmserver_module.CONFIG_FILE = temp_config_file

            try:
                config = LLMServerConfig()

                # Should load partial data
                assert config.MODEL_PATH == "/partial/model.gguf"
                assert config.LISTEN_PORT == 7000
                # Should keep defaults for unspecified values
                assert config.THREAD_COUNT == 4  # Default value
            finally:
                # Restore original CONFIG_FILE
                llmserver_module.CONFIG_FILE = original_config_file

    def test_config_save_failure(self) -> None:
        """Test configuration save failure handling."""
        # Since mkdir(parents=True) creates directories automatically,
        # we test a different failure scenario
        with tempfile.TemporaryDirectory() as temp_dir:
            # Create a file where we expect a directory
            conflicting_path = Path(temp_dir) / "llmserver.json"
            conflicting_path.write_text("this is a file, not a directory")

            # Now try to create a file inside what should be a directory
            temp_config_file = conflicting_path / "config.json"

            # Patch the global CONFIG_FILE
            import pytola.llm.llmserver.gui as llmserver_module

            original_config_file = llmserver_module.CONFIG_FILE
            llmserver_module.CONFIG_FILE = temp_config_file

            try:
                config = LLMServerConfig()

                with pytest.raises(Exception, match="Failed to save config"):
                    config.save()
            finally:
                # Restore original CONFIG_FILE
                llmserver_module.CONFIG_FILE = original_config_file

    def test_config_load_invalid_json(self) -> None:
        """Test handling of invalid JSON in config file."""
        with tempfile.TemporaryDirectory() as temp_dir:
            temp_config_file = Path(temp_dir) / "llmserver.json"

            # Create invalid JSON file
            temp_config_file.write_text("{invalid json}")

            # Patch the global CONFIG_FILE
            import pytola.llm.llmserver.gui as llmserver_module

            original_config_file = llmserver_module.CONFIG_FILE
            llmserver_module.CONFIG_FILE = temp_config_file

            try:
                # Should not crash, should use defaults
                config = LLMServerConfig()
                assert config.MODEL_PATH == ""  # Default value
            finally:
                # Restore original CONFIG_FILE
                llmserver_module.CONFIG_FILE = original_config_file

    def test_config_class_variables_not_serialized(self) -> None:
        """Test that class variables are not saved to config file."""
        with tempfile.TemporaryDirectory() as temp_dir:
            temp_config_file = Path(temp_dir) / "llmserver.json"

            # Patch the global CONFIG_FILE
            import pytola.llm.llmserver.gui as llmserver_module

            original_config_file = llmserver_module.CONFIG_FILE
            llmserver_module.CONFIG_FILE = temp_config_file

            try:
                config = LLMServerConfig()
                config.save()

                # Read the saved file
                saved_data = json.loads(temp_config_file.read_text())

                # Class variables should not be in the saved data
                assert "TITLE" not in saved_data
                assert "WIN_SIZE" not in saved_data
                assert "WIN_POS" not in saved_data
                assert "LISTEN_PORT_RNG" not in saved_data
                assert "THREAD_COUNT_RNG" not in saved_data

                # Instance variables should be present
                assert "MODEL_PATH" in saved_data
                assert "LISTEN_PORT" in saved_data
                assert "THREAD_COUNT" in saved_data
            finally:
                # Restore original CONFIG_FILE
                llmserver_module.CONFIG_FILE = original_config_file


class TestLlamaServerGUI:
    """Tests for LlamaServerGUI component."""

    @pytest.mark.skip(reason="GUI tests cause UI interference, skipping to avoid user experience issues")
    def test_gui_initialization(self, qtbot: QtBot) -> None:
        """Test GUI initialization."""
        gui = LlamaServerGUI()
        qtbot.addWidget(gui)

        # Verify GUI is created successfully
        assert gui is not None
        assert hasattr(gui, "process")
        assert hasattr(gui, "model_path_input")
        assert hasattr(gui, "port_spin")
        assert hasattr(gui, "threads_spin")

    @pytest.mark.skip(reason="GUI tests cause UI interference, skipping to avoid user experience issues")
    def test_gui_default_values(self, qtbot: QtBot) -> None:
        """Test GUI default values are set correctly."""
        gui = LlamaServerGUI()
        qtbot.addWidget(gui)

        # Check default values
        assert gui.port_spin.value() == 8080
        assert gui.threads_spin.value() == 4
        assert gui.model_path_input.placeholderText() == "Choose model file..."

    @pytest.mark.skip(reason="GUI tests cause UI interference, skipping to avoid user experience issues")
    def test_gui_config_loading(self, qtbot: QtBot) -> None:
        """Test GUI loads configuration correctly."""
        with tempfile.TemporaryDirectory() as temp_dir:
            temp_config_file = Path(temp_dir) / "llmserver.json"
            config_data = {
                "MODEL_PATH": "/test/model.gguf",
                "LISTEN_PORT": 9000,
                "THREAD_COUNT": 6,
            }
            temp_config_file.write_text(json.dumps(config_data))

            with patch("pytola.llm.llmserver.gui.CONFIG_FILE", temp_config_file):
                gui = LlamaServerGUI()
                qtbot.addWidget(gui)

                # Verify GUI reflects loaded configuration
                assert gui.model_path_input.text() == "/test/model.gguf"
                assert gui.port_spin.value() == 9000
                assert gui.threads_spin.value() == 6

    @pytest.mark.skip(reason="GUI tests cause UI interference, skipping to avoid user experience issues")
    def test_model_selection_dialog(self, qtbot: QtBot) -> None:
        """Test model file selection dialog method exists."""
        gui = LlamaServerGUI()
        qtbot.addWidget(gui)

        # Test that the method exists and is callable
        assert hasattr(gui, "on_load_model")
        assert callable(gui.on_load_model)

    @pytest.mark.skip(reason="GUI tests cause UI interference, skipping to avoid user experience issues")
    def test_server_control_methods(self, qtbot: QtBot) -> None:
        """Test server control methods exist."""
        gui = LlamaServerGUI()
        qtbot.addWidget(gui)

        # Test method existence
        assert hasattr(gui, "toggle_server")
        assert hasattr(gui, "start_server")
        assert hasattr(gui, "stop_server")
        assert callable(gui.toggle_server)
        assert callable(gui.start_server)
        assert callable(gui.stop_server)

    @pytest.mark.skip(reason="GUI tests cause UI interference, skipping to avoid user experience issues")
    def test_browser_launch_method(self, qtbot: QtBot) -> None:
        """Test browser launch method exists."""
        gui = LlamaServerGUI()
        qtbot.addWidget(gui)

        assert hasattr(gui, "on_start_browser")
        assert callable(gui.on_start_browser)

    @pytest.mark.skip(reason="GUI tests cause UI interference, skipping to avoid user experience issues")
    def test_output_formatting_methods(self, qtbot: QtBot) -> None:
        """Test output formatting methods exist."""
        gui = LlamaServerGUI()
        qtbot.addWidget(gui)

        assert hasattr(gui, "append_output")
        assert hasattr(gui, "create_text_format")
        assert callable(gui.append_output)
        assert callable(gui.create_text_format)

    @pytest.mark.skip(reason="GUI tests cause UI interference, skipping to avoid user experience issues")
    def test_ui_state_updates(self, qtbot: QtBot) -> None:
        """Test UI state update methods exist."""
        gui = LlamaServerGUI()
        qtbot.addWidget(gui)

        assert hasattr(gui, "update_ui_state")
        assert callable(gui.update_ui_state)

    @pytest.mark.skip(reason="GUI tests cause UI interference, skipping to avoid user experience issues")
    def test_window_event_handlers(self, qtbot: QtBot) -> None:
        """Test window event handlers exist."""
        gui = LlamaServerGUI()
        qtbot.addWidget(gui)

        assert hasattr(gui, "moveEvent")
        assert hasattr(gui, "resizeEvent")
        assert hasattr(gui, "closeEvent")
        assert callable(gui.moveEvent)
        assert callable(gui.resizeEvent)
        assert callable(gui.closeEvent)


class TestIntegration:
    """Integration tests for llmserver module."""

    def test_import_main_function(self) -> None:
        """Test that main function can be imported."""
        from pytola.llm.llmserver.gui import main

        assert callable(main)

    def test_config_import(self) -> None:
        """Test that configuration class can be imported."""
        from pytola.llm.llmserver.gui import LLMServerConfig

        config = LLMServerConfig()
        assert isinstance(config, LLMServerConfig)

    def test_gui_import(self) -> None:
        """Test that GUI class can be imported."""
        from pytola.llm.llmserver.gui import LlamaServerGUI

        # Don't instantiate to avoid GUI display
        assert LlamaServerGUI.__name__ == "LlamaServerGUI"


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
