"""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 hypothesis import given
from hypothesis import strategies as st

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

    @given(
        st.text(
            alphabet=st.characters(min_codepoint=97, max_codepoint=122),  # lowercase letters
            min_size=1,
            max_size=20,
        )
    )
    def test_process_gguf_stem_without_f16_suffix_property_based(self, filename):
        """Property-based test for filenames without F16 suffix using hypothesis."""
        # 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

    @given(
        st.text(
            alphabet=st.characters(min_codepoint=97, max_codepoint=122),
            min_size=1,
            max_size=20,
        )
    )
    def test_process_gguf_stem_with_f16_suffix_property_based(self, base_filename):
        """Property-based test for filenames with F16 suffix using hypothesis."""
        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):
        """Test default configuration initialization."""
        # Create a temporary config file path to avoid affecting real config
        with mock.patch("pytola.llm.llmquantize.gui.CONFIG_FILE", Path("/tmp/test_config.json")):
            config = QuantizerConfig()

            assert config.TITLE == "GGUF量化转换工具"
            assert config.WIN_SIZE == [600, 500]
            assert config.WIN_POS == [100, 100]
            assert 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(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(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"]

    @given(st.lists(st.integers(min_value=100, max_value=2000), min_size=2, max_size=2))
    def test_config_win_size_property_based(self, win_size):
        """Property-based test for WIN_SIZE configuration using hypothesis."""
        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

    @given(st.lists(st.integers(min_value=0, max_value=1000), min_size=2, max_size=2))
    def test_config_win_pos_property_based(self, win_pos):
        """Property-based test for WIN_POS configuration using hypothesis."""
        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(lambda msg: progress_messages.append(msg))
            worker.progress_count_updated.connect(lambda count: progress_counts.append(count))
            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(lambda msg: progress_messages.append(msg))

            worker.run()

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

    @given(st.lists(st.sampled_from(["Q4_K_M", "Q5_K_M", "Q6_K"]), min_size=1, max_size=5))
    def test_worker_multiple_quant_types_property_based(self, quant_types):
        """Property-based test for multiple quantization types using hypothesis."""
        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"])
