"""Test suite for llmquantize module."""

from __future__ import annotations

import json
import logging
import tempfile
from pathlib import Path
from unittest import mock

import pytest

from pytola_llm.llmquantize.gui import (
    GGUFQuantizerGUI,
    QuantizationWorker,
    QuantizerConfig,
    _process_gguf_stem,
)

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


class TestProcessGGUFStem:
    """Test cases for _process_gguf_stem function."""

    @pytest.mark.parametrize(
        ("filename", "expected_result"),
        [
            ("model-F16", "model"),  # Standard F16 suffix uppercase
            ("llama-2-F16", "llama-2"),  # Model name with hyphen and F16 suffix
            ("model-f16", "model"),  # Lowercase f16 also removed (case-insensitive)
            ("model", "model"),  # No F16 suffix
            ("model-F16-quantized", "model-F16-quantized"),  # F16 not at the end
            ("F16", "F16"),  # Only F16 text
            ("model-F16-F16", "model-F16"),  # Multiple F16 (only last one removed)
            ("", ""),  # Empty string
            ("my-model-v2-F16", "my-model-v2"),  # Complex model name with F16
        ],
    )
    def test_process_gguf_stem_various_cases(self, filename, expected_result):
        """Test _process_gguf_stem with various filename patterns."""
        result = _process_gguf_stem(filename)
        assert result == expected_result

    @pytest.mark.parametrize("filename", ["abcdef", "hello", "mymodel", "testfile", "xyz"])
    def test_process_gguf_stem_without_f16_suffix_property_based(self, filename):
        """Property-based test for filenames without F16 suffix."""
        # Ensure input doesn't end with -F16
        if filename.upper().endswith("-F16"):
            filename = filename[:-4] + "xx"

        result = _process_gguf_stem(filename)
        # Result should be same as input when no F16 suffix
        assert result == filename

    @pytest.mark.parametrize("base_filename", ["abcdef", "hello", "mymodel", "testfile", "xyz"])
    def test_process_gguf_stem_with_f16_suffix_property_based(self, base_filename):
        """Property-based test for filenames with F16 suffix."""
        filename = f"{base_filename}-F16"
        result = _process_gguf_stem(filename)
        assert result == base_filename


class TestQuantizerConfig:
    """Test cases for QuantizerConfig class."""

    def test_default_config_initialization(self, tmp_path: Path):
        """Test default configuration initialization."""
        # Create a temporary config file path to avoid affecting real config
        with mock.patch("pytola_llm.llmquantize.gui.CONFIG_FILE", tmp_path / "test_config.json"):
            config = QuantizerConfig()

            assert config.TITLE == "GGUF量化转换工具"
            assert config.WIN_SIZE == [600, 500]
            assert config.WIN_POS == [100, 100]
            assert not config.LAST_INPUT_FILE
            assert config.SELECTED_QUANTS == ["Q4_K_M", "Q5_K_M"]

    def test_config_load_from_file(self):
        """Test loading configuration from file."""
        with tempfile.NamedTemporaryFile(encoding="utf-8", mode="w", suffix=".json", delete=False) as tmp_file:
            tmp_path = Path(tmp_file.name)
            config_data = {
                "WIN_SIZE": [800, 600],
                "WIN_POS": [200, 150],
                "LAST_INPUT_FILE": "/test/path/model.gguf",
                "SELECTED_QUANTS": ["Q4_K_M"],
            }
            tmp_file.write(json.dumps(config_data))
            tmp_file.flush()

        try:
            with mock.patch("pytola_llm.llmquantize.gui.CONFIG_FILE", tmp_path):
                config = QuantizerConfig()

                assert config.WIN_SIZE == [800, 600]
                assert config.WIN_POS == [200, 150]
                assert config.LAST_INPUT_FILE == "/test/path/model.gguf"
                assert config.SELECTED_QUANTS == ["Q4_K_M"]
        finally:
            tmp_path.unlink()

    def test_config_load_with_invalid_json(self):
        """Test loading configuration with invalid JSON."""
        with tempfile.NamedTemporaryFile(encoding="utf-8", mode="w", suffix=".json", delete=False) as tmp_file:
            tmp_path = Path(tmp_file.name)
            tmp_file.write("invalid json content")
            tmp_file.flush()

        try:
            with mock.patch("pytola_llm.llmquantize.gui.CONFIG_FILE", tmp_path):
                config = QuantizerConfig()

                # Should fall back to defaults
                assert config.TITLE == "GGUF量化转换工具"
                assert config.WIN_SIZE == [600, 500]
        finally:
            tmp_path.unlink()

    def test_config_save(self):
        """Test saving configuration to file."""
        with tempfile.TemporaryDirectory() as tmp_dir:
            tmp_path = Path(tmp_dir) / "test_config.json"

            with mock.patch("pytola_llm.llmquantize.gui.CONFIG_FILE", tmp_path):
                config = QuantizerConfig()
                config.WIN_SIZE = [1024, 768]
                config.WIN_POS = [300, 200]
                config.LAST_INPUT_FILE = "/custom/path/model.gguf"
                config.SELECTED_QUANTS = ["Q5_K_M", "Q6_K"]

                config.save()

                # Verify file was created
                assert tmp_path.exists()

                # Verify content
                saved_data = json.loads(tmp_path.read_text())
                assert saved_data["WIN_SIZE"] == [1024, 768]
                assert saved_data["WIN_POS"] == [300, 200]
                assert saved_data["LAST_INPUT_FILE"] == "/custom/path/model.gguf"
                assert saved_data["SELECTED_QUANTS"] == ["Q5_K_M", "Q6_K"]

    @pytest.mark.parametrize("win_size", [[100, 200], [800, 600], [1920, 1080], [2000, 2000]])
    def test_config_win_size_property_based(self, win_size):
        """Property-based test for WIN_SIZE configuration."""
        with tempfile.TemporaryDirectory() as tmp_dir:
            tmp_path = Path(tmp_dir) / "test_config.json"

            with mock.patch("pytola_llm.llmquantize.gui.CONFIG_FILE", tmp_path):
                config = QuantizerConfig()
                config.WIN_SIZE = win_size
                config.save()

                # Load and verify
                saved_data = json.loads(tmp_path.read_text())
                assert saved_data["WIN_SIZE"] == win_size

    @pytest.mark.parametrize("win_pos", [[0, 0], [100, 200], [500, 500], [1000, 1000]])
    def test_config_win_pos_property_based(self, win_pos):
        """Property-based test for WIN_POS configuration."""
        with tempfile.TemporaryDirectory() as tmp_dir:
            tmp_path = Path(tmp_dir) / "test_config.json"

            with mock.patch("pytola_llm.llmquantize.gui.CONFIG_FILE", tmp_path):
                config = QuantizerConfig()
                config.WIN_POS = win_pos
                config.save()

                # Load and verify
                saved_data = json.loads(tmp_path.read_text())
                assert saved_data["WIN_POS"] == win_pos


class TestQuantizationWorker:
    """Test cases for QuantizationWorker class."""

    def test_worker_initialization(self):
        """Test QuantizationWorker initialization."""
        with tempfile.TemporaryDirectory() as tmp_dir:
            input_file = Path(tmp_dir) / "model-F16.gguf"
            input_file.write_text("mock model content")

            quant_types = ["Q4_K_M", "Q5_K_M"]
            worker = QuantizationWorker(input_file, quant_types)

            assert worker.input_file == input_file
            assert worker.quant_types == quant_types
            assert worker.input_dir == input_file.parent
            assert worker.base_name == "model"  # F16 should be removed
            assert worker.total_files == 2
            assert worker.completed_files == 0
            assert worker.success is True

    def test_worker_base_name_without_f16(self):
        """Test base name extraction without F16 suffix."""
        with tempfile.TemporaryDirectory() as tmp_dir:
            input_file = Path(tmp_dir) / "mymodel.gguf"
            input_file.write_text("mock model content")

            worker = QuantizationWorker(input_file, ["Q4_K_M"])

            assert worker.base_name == "mymodel"

    @mock.patch("subprocess.Popen")
    def test_worker_run_successful_conversion(self, mock_popen):
        """Test successful conversion execution."""
        with tempfile.TemporaryDirectory() as tmp_dir:
            input_file = Path(tmp_dir) / "model-F16.gguf"
            input_file.write_text("mock model content")

            # Mock subprocess with proper IO behavior
            mock_process = mock.Mock()
            # Create a mock file-like object for stdout
            mock_stdout = mock.Mock()
            mock_stdout.__iter__ = mock.Mock(return_value=iter(["Converting...", "Complete"]))
            mock_stdout.close = mock.Mock()
            mock_process.stdout = mock_stdout
            mock_process.returncode = 0
            mock_process.wait.return_value = None
            mock_popen.return_value = mock_process

            worker = QuantizationWorker(input_file, ["Q4_K_M"])

            # Collect emitted signals
            progress_messages = []
            progress_counts = []
            finished = []

            worker.progress_msg_updated.connect(progress_messages.append)
            worker.progress_count_updated.connect(progress_counts.append)
            worker.is_finished.connect(lambda: finished.append(True))

            worker.run()

            # Verify signals were emitted
            assert len(progress_messages) > 0
            assert len(progress_counts) > 0
            assert len(finished) == 1
            assert worker.success is True
            assert worker.completed_files == 1

    @mock.patch("subprocess.Popen")
    def test_worker_run_failed_conversion(self, mock_popen):
        """Test failed conversion execution."""
        with tempfile.TemporaryDirectory() as tmp_dir:
            input_file = Path(tmp_dir) / "model-F16.gguf"
            input_file.write_text("mock model content")

            # Mock subprocess failure
            mock_process = mock.Mock()
            mock_stdout = mock.Mock()
            mock_stdout.__iter__ = mock.Mock(return_value=iter(["Error occurred"]))
            mock_stdout.close = mock.Mock()
            mock_process.stdout = mock_stdout
            mock_process.returncode = 1
            mock_process.wait.return_value = None
            mock_popen.return_value = mock_process

            worker = QuantizationWorker(input_file, ["Q4_K_M"])

            finished = []
            worker.is_finished.connect(lambda: finished.append(True))

            worker.run()

            # Verify failure was handled
            assert len(finished) == 1
            assert worker.success is False
            assert worker.completed_files == 0

    @mock.patch("subprocess.Popen")
    def test_worker_run_with_permission_error(self, mock_popen):
        """Test conversion with permission error."""
        with tempfile.TemporaryDirectory() as tmp_dir:
            input_file = Path(tmp_dir) / "model-F16.gguf"
            input_file.write_text("mock model content")

            # Mock permission error
            mock_popen.side_effect = PermissionError("Permission denied")

            worker = QuantizationWorker(input_file, ["Q4_K_M"])

            progress_messages = []
            worker.progress_msg_updated.connect(progress_messages.append)

            worker.run()

            # Verify error was handled
            assert worker.success is False
            assert any("无法启动量化进程" in msg for msg in progress_messages)

    @pytest.mark.parametrize("quant_types", [["Q4_K_M"], ["Q5_K_M", "Q6_K"], ["Q4_K_M", "Q5_K_M", "Q6_K"]])
    def test_worker_multiple_quant_types_property_based(self, quant_types):
        """Property-based test for multiple quantization types."""
        with tempfile.TemporaryDirectory() as tmp_dir:
            input_file = Path(tmp_dir) / "model-F16.gguf"
            input_file.write_text("mock model content")

            worker = QuantizationWorker(input_file, quant_types)

            assert worker.total_files == len(quant_types)
            assert worker.quant_types == quant_types


class TestGGUFQuantizerGUI:
    """Test cases for GGUFQuantizerGUI class."""

    def test_gui_check_existing_quant_files(self, qtbot):
        """Test checking existing quantization files."""
        with tempfile.TemporaryDirectory() as tmp_dir:
            tmp_path = Path(tmp_dir)

            # Create test input file
            input_file = tmp_path / "model-F16.gguf"
            input_file.write_text("mock model content")

            # Create existing quantization file
            existing_quant = tmp_path / "model-Q4_K_M.gguf"
            existing_quant.write_text("existing quantized model")

            tmp_config = tmp_path / "test_gui.json"
            with mock.patch("pytola_llm.llmquantize.gui.CONFIG_FILE", tmp_config):
                gui = GGUFQuantizerGUI()
                qtbot.addWidget(gui)

                gui.input_file = input_file
                gui.check_existing_quant_files()

                # Verify checkbox for existing file is marked
                checkbox_text = gui.quant_checks["Q4_K_M"].text()
                assert "(已存在)" in checkbox_text

                # Verify checkbox for non-existing file is not marked
                checkbox_text_q5 = gui.quant_checks["Q5_K_M"].text()
                assert "(已存在)" not in checkbox_text_q5

    def test_gui_on_quant_type_changed(self, qtbot):
        """Test quantization type change handler."""
        with tempfile.TemporaryDirectory() as tmp_dir:
            tmp_config = Path(tmp_dir) / "test_gui.json"

            with mock.patch("pytola_llm.llmquantize.gui.CONFIG_FILE", tmp_config):
                gui = GGUFQuantizerGUI()
                qtbot.addWidget(gui)

                # Simulate checking some quant types
                gui.quant_checks["Q4_K_M"].setChecked(True)
                gui.quant_checks["Q6_K"].setChecked(True)

                # Verify config was updated (would be called in real scenario)
                assert "Q4_K_M" in [q for q, check in gui.quant_checks.items() if check.isChecked()]
                assert "Q6_K" in [q for q, check in gui.quant_checks.items() if check.isChecked()]

    def test_gui_move_event_saves_position(self, qtbot):
        """Test that moving window saves position to config."""
        with tempfile.TemporaryDirectory() as tmp_dir:
            tmp_config = Path(tmp_dir) / "test_gui.json"

            with mock.patch("pytola_llm.llmquantize.gui.CONFIG_FILE", tmp_config), mock.patch(
                "pytola_llm.llmquantize.gui.conf",
            ) as mock_conf:
                mock_conf.TITLE = "GGUF量化转换工具"
                mock_conf.WIN_POS = [100, 100]
                mock_conf.WIN_SIZE = [600, 500]
                mock_conf.LAST_INPUT_FILE = ""
                mock_conf.SELECTED_QUANTS = ["Q4_K_M", "Q5_K_M"]
                gui = GGUFQuantizerGUI()
                qtbot.addWidget(gui)

                # Move window
                gui.move(250, 300)
                qtbot.wait(10)  # Wait for event processing

                # Verify position was saved to config
                # Note: Actual position may vary slightly due to window decorations
                assert isinstance(mock_conf.WIN_POS, list)
                assert len(mock_conf.WIN_POS) == 2

    def test_gui_resize_event_saves_size(self, qtbot):
        """Test that resizing window saves size to config."""
        with tempfile.TemporaryDirectory() as tmp_dir:
            tmp_config = Path(tmp_dir) / "test_gui.json"

            with mock.patch("pytola_llm.llmquantize.gui.CONFIG_FILE", tmp_config), mock.patch(
                "pytola_llm.llmquantize.gui.conf",
            ) as mock_conf:
                mock_conf.TITLE = "GGUF量化转换工具"
                mock_conf.WIN_SIZE = [600, 500]
                mock_conf.WIN_POS = [100, 100]
                mock_conf.LAST_INPUT_FILE = ""
                mock_conf.SELECTED_QUANTS = ["Q4_K_M", "Q5_K_M"]
                gui = GGUFQuantizerGUI()
                qtbot.addWidget(gui)

                # Resize window
                gui.resize(800, 600)
                qtbot.wait(10)  # Wait for event processing

                # Verify size was saved to config
                assert isinstance(mock_conf.WIN_SIZE, list)
                assert len(mock_conf.WIN_SIZE) == 2


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

    def test_full_config_workflow(self):
        """Test complete configuration workflow."""
        with tempfile.TemporaryDirectory() as tmp_dir:
            tmp_path = Path(tmp_dir) / "config.json"

            with mock.patch("pytola_llm.llmquantize.gui.CONFIG_FILE", tmp_path):
                # Create and configure
                config1 = QuantizerConfig()
                config1.WIN_SIZE = [1024, 768]
                config1.WIN_POS = [200, 150]
                config1.LAST_INPUT_FILE = "/path/to/model.gguf"
                config1.SELECTED_QUANTS = ["Q4_K_M", "Q6_K"]
                config1.save()

                # Load and verify
                config2 = QuantizerConfig()
                assert config2.WIN_SIZE == [1024, 768]
                assert config2.WIN_POS == [200, 150]
                assert config2.LAST_INPUT_FILE == "/path/to/model.gguf"
                assert config2.SELECTED_QUANTS == ["Q4_K_M", "Q6_K"]

    @pytest.mark.parametrize(
        ("input_filename", "expected_base"),
        [
            ("model-F16.gguf", "model"),
            ("llama-2-7b-F16.gguf", "llama-2-7b"),
            ("custom-model-F16.gguf", "custom-model"),
            ("simple.gguf", "simple"),
        ],
    )
    def test_worker_base_name_extraction_parametrized(self, input_filename, expected_base):
        """Parametrized test for base name extraction in worker."""
        with tempfile.TemporaryDirectory() as tmp_dir:
            input_file = Path(tmp_dir) / input_filename
            input_file.write_text("mock model content")

            worker = QuantizationWorker(input_file, ["Q4_K_M"])
            assert worker.base_name == expected_base


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