"""Tests for dicominfo module."""

# ruff: noqa: S101 PLR2004

# Python imports
import importlib
from collections.abc import Callable
from pathlib import Path
from unittest.mock import MagicMock, patch

# Module Imports
import matplotlib
import numpy as np
import pytest
from dicominfo import (
    DicomReadError,
    NoPixelDataError,
    UnsupportedPixelDataError,
    display_images,
    print_stats,
)
from dicominfo.core import validate_files
from dicominfo.utils import load_dicom_files
from dicominfo.viewer import _create_slice_updater, _get_image_type

matplotlib.use('Agg')

class TestLazyImport:
    """Tests for __getattr__ lazy loading mechanism."""

    def test_display_images_lazy_import(self) -> None:
        """Test that display_images is loaded lazily."""
        import sys

        # Remove dicominfo modules from cache to test fresh import
        modules_to_remove = [
            mod for mod in sys.modules if mod.startswith("dicominfo")
        ]
        for mod in modules_to_remove:
            del sys.modules[mod]

        # Import dicominfo fresh
        import dicominfo

        # Verify viewer not yet loaded
        assert "dicominfo.viewer" not in sys.modules

        # Access display_images (triggers __getattr__)
        func = dicominfo.display_images

        # Verify it was imported correctly
        assert "dicominfo.viewer" in sys.modules
        assert callable(func)

    def test_invalid_attribute_raises_error(self) -> None:
        """Test that invalid attributes raise AttributeError."""
        import dicominfo

        with pytest.raises(AttributeError, match="module 'dicominfo' has no attribute 'nonexistent'"):
            _ = dicominfo.nonexistent

    def test_other_attributes_work_normally(self) -> None:
        """Test that non-lazy attributes are accessible immediately."""
        import dicominfo

        # These should work without triggering lazy import of viewer
        assert hasattr(dicominfo, "__version__")
        assert hasattr(dicominfo, "DicomReadError")
        assert hasattr(dicominfo, "main")


class TestVersion:
    """Tests for version module."""

    def test_version_is_string(self) -> None:
        """Test that __version__ is a string."""
        from dicominfo import __version__

        assert isinstance(__version__, str)

    def test_version_matches_semantic_versioning(self) -> None:
        """Test that version follows semver pattern (x.y.z)."""
        import re

        from dicominfo import __version__

        # Match semantic versioning: MAJOR.MINOR.PATCH[-prerelease][+build]
        semver_pattern = r"^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$"
        assert re.match(semver_pattern, __version__), (
            f"Version '{__version__}' does not match semantic versioning"
        )

    def test_version_importable_from_root(self) -> None:
        """Test that version is importable from package root."""
        from dicominfo import __version__

        assert __version__ is not None
        assert len(__version__) > 0

    @patch("importlib.metadata.version")
    def test_dev_version(
        self,
        mock_metadata_version: Callable,
    ) -> None:
        """Test that the version is dev version.

        When the package cannot be installed.

        """
        from importlib.metadata import PackageNotFoundError
        mock_metadata_version.side_effect = PackageNotFoundError

        from dicominfo import _version
        importlib.reload(_version)

        assert "+dev" in _version.__version__

    @patch("tomllib.load")
    @patch("importlib.metadata.version")
    def test_toml_decode_error(
        self,
        mock_metadata_version: Callable,
        mock_tomllib_load: Callable,
    ) -> None:
        """Check a runtime error is raised on TOMLDecodeError."""
        from importlib.metadata import PackageNotFoundError
        mock_metadata_version.side_effect = PackageNotFoundError

        from dicominfo import _version

        from tomllib import TOMLDecodeError
        mock_tomllib_load.side_effect = TOMLDecodeError

        with pytest.raises(RuntimeError, match="Failed to parse"):
            importlib.reload(_version)


class TestLoadDicomFiles:
    """Tests for load_dicom_files function."""

    def test_raises_dicom_read_error_on_file_not_found(self) -> None:
        """Test that load_dicom_files raises DicomReadError when file is not found."""
        with pytest.raises(DicomReadError, match="Files could not be read"):
            load_dicom_files(["/nonexistent/file.dcm"])

    def test_raises_dicom_read_error_on_invalid_dicom(
        self, tmp_path: Path
    ) -> None:
        """Test that load_dicom_files raises DicomReadError on invalid DICOM file."""
        # Create a non-DICOM file
        invalid_file = tmp_path / "invalid.dcm"
        invalid_file.write_text("This is not a DICOM file")

        with pytest.raises(DicomReadError, match="Files could not be read"):
            load_dicom_files([str(invalid_file)])

    @patch("dicominfo.utils.pydicom.dcmread")
    def test_returns_list_of_datasets(self, mock_dcmread: Callable) -> None:
        """Test that load_dicom_files returns a list of pydicom Dataset objects."""
        # Mock pydicom.dcmread to return mock Dataset objects
        mock_dcm1 = MagicMock()
        mock_dcm2 = MagicMock()
        mock_dcmread.side_effect = [mock_dcm1, mock_dcm2]

        result = load_dicom_files(["file1.dcm", "file2.dcm"])

        assert len(result) == 2
        assert result[0] == mock_dcm1
        assert result[1] == mock_dcm2
        assert mock_dcmread.call_count == 2


class TestValidateFiles:
    """Tests for validate_files function."""

    def test_raises_dicom_read_error_on_file_not_found(self) -> None:
        """Test that validate_files raises DicomReadError when file is not found."""
        with pytest.raises(DicomReadError, match="Files could not be read"):
            validate_files(["/nonexistent/file.dcm"])

    def test_raises_dicom_read_error_on_invalid_dicom(
        self, tmp_path: Path
    ) -> None:
        """Test that validate_files raises DicomReadError on invalid DICOM file."""
        # Create a non-DICOM file
        invalid_file = tmp_path / "invalid.dcm"
        invalid_file.write_text("This is not a DICOM file")

        with pytest.raises(DicomReadError, match="Files could not be read"):
            validate_files([str(invalid_file)])


class TestPrintStats:
    """Tests for print_stats function."""

    def test_raises_dicom_read_error_on_file_not_found(self) -> None:
        """Test that print_stats raises DicomReadError when file is not found."""
        with pytest.raises(DicomReadError, match="Files could not be read"):
            print_stats(["/nonexistent/file.dcm"])

    def test_raises_dicom_read_error_on_invalid_dicom(
        self, tmp_path: Path
    ) -> None:
        """Test that print_stats raises DicomReadError on invalid DICOM file."""
        # Create a non-DICOM file
        invalid_file = tmp_path / "invalid.dcm"
        invalid_file.write_text("This is not a DICOM file")

        with pytest.raises(DicomReadError, match="Files could not be read"):
            print_stats([str(invalid_file)])


class TestDisplayImages:
    """Tests for display_images function."""

    def test_raises_dicom_read_error_on_file_not_found(self) -> None:
        """Test that display_images raises DicomReadError when file is not found."""
        with pytest.raises(DicomReadError, match="Files could not be read"):
            display_images(["/nonexistent/file.dcm"])

    def test_raises_dicom_read_error_on_invalid_dicom(
        self, tmp_path: Path
    ) -> None:
        """Test that display_images raises DicomReadError on invalid DICOM file."""
        # Create a non-DICOM file
        invalid_file = tmp_path / "invalid.dcm"
        invalid_file.write_text("This is not a DICOM file")

        with pytest.raises(DicomReadError, match="Files could not be read"):
            display_images([str(invalid_file)])

    @patch("dicominfo.utils.pydicom.dcmread")
    def test_raises_no_pixel_data_error_when_no_pixel_data(
        self, mock_dcmread: Callable
    ) -> None:
        """Test that display_images raises NoPixelDataError.

        When files have no pixel data.

        """
        # Mock a DICOM file without pixel_array attribute
        mock_dcm = MagicMock()
        del mock_dcm.pixel_array  # Ensure pixel_array doesn't exist
        mock_dcmread.return_value = mock_dcm

        with pytest.raises(
            NoPixelDataError, match="No DICOM files with pixel data found"
        ):
            display_images(["mock_file.dcm"])

    @patch("dicominfo.utils.pydicom.dcmread")
    def test_raises_unsupported_pixel_data_error_with_unknown_image_type(
        self, mock_dcmread: Callable
    ) -> None:
        """Test that display_images raises UnsupportedPixelDataError.

        When files have incorrect pixel data shape.
        """
        # Mock a DICOM file with 4D pixel array (e.g., time series with multiple slices)
        mock_dcm = MagicMock()
        mock_dcm.SamplesPerPixel = 1
        mock_dcm.NumberOfFrames = 10
        # 4D array: (time, slices, height, width) - currently unsupported
        mock_dcm.pixel_array = np.zeros((5, 10, 256, 256), dtype=np.uint16)
        mock_dcmread.return_value = mock_dcm

        with pytest.raises(
            UnsupportedPixelDataError,
            match="has unsupported dimensions",
        ):
            display_images(["mock_file.dcm"])


class TestSliceUpdater:
    """Tests for the slider update callback logic."""

    def test_update_sets_correct_slice(self) -> None:
        """Test that update changes the image to the correct slice."""
        # Arrange
        mock_fig = MagicMock()
        mock_ax = MagicMock()
        mock_im = MagicMock()
        mock_slider = MagicMock()

        # 3D volume: 5 slices of 10x10
        data = np.random.rand(5, 10, 10).astype(np.float32)

        updater = _create_slice_updater(
            mock_im, mock_ax, data, "test.dcm", mock_slider, mock_fig
        )

        # Act - simulate slider moved to index 3
        mock_slider.val = 3
        updater(3.0)

        # Assert
        assert mock_im.set_data.call_count == 1
        # Use numpy testing to compare arrays properly
        passed_array = mock_im.set_data.call_args[0][0]
        np.testing.assert_array_equal(passed_array, data[3])
        mock_ax.set_title.assert_called_once_with("test.dcm\nSlice 4/5")
        mock_fig.canvas.draw_idle.assert_called_once()

    def test_update_logs_debug_message(self, caplog: pytest.LogCaptureFixture) -> None:
        """Test that update logs the slider value."""
        import logging

        mock_fig = MagicMock()
        mock_ax = MagicMock()
        mock_im = MagicMock()
        mock_slider = MagicMock()
        mock_slider.val = 2

        updater = _create_slice_updater(
            mock_im, mock_ax, np.zeros((5, 10, 10)), "file.dcm", mock_slider, mock_fig
        )

        with caplog.at_level(logging.DEBUG, logger="dicominfo.viewer"):
            updater(2.5)  # Note: function uses int(sldr.val), not the parameter

        assert "Slider at slice 2.500000 for file.dcm" in caplog.text


class TestMain:
    """Tests for main CLI function."""

    @patch("dicominfo.viewer.display_images")
    @patch("dicominfo.cli.print_stats")
    @patch("sys.argv", ["dicom-info", "--display", "mock_file.dcm"])
    def test_exits_with_code_1_on_unsupported_pixel_error(
        self,
        mock_print_stats: Callable,  # noqa: ARG002
        mock_display_images: Callable,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        """Test that main exits with code 1 on UnsupportedPixelDataError."""
        from dicominfo import main
        from dicominfo.exceptions import UnsupportedPixelDataError

        # Mock print_stats to raise DicomReadError
        mock_display_images.side_effect = UnsupportedPixelDataError(
            "Unsupported Pixel Data",
        )

        with pytest.raises(SystemExit) as exc_info:
            main()

        assert exc_info.value.code == 1
        captured = capsys.readouterr()
        assert "Unsupported Pixel Data" in captured.out


    @patch("dicominfo.cli.print_stats")
    @patch("sys.argv", ["dicom-info", "/nonexistent/file.dcm"])
    def test_exits_with_code_1_on_dicom_read_error(
        self, mock_print_stats: Callable, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """Test that main exits with code 1 when DicomReadError is raised."""
        from dicominfo import main
        from dicominfo.exceptions import DicomReadError

        # Mock print_stats to raise DicomReadError
        mock_print_stats.side_effect = DicomReadError("Test error message")

        with pytest.raises(SystemExit) as exc_info:
            main()

        assert exc_info.value.code == 1
        captured = capsys.readouterr()
        assert "Test error message" in captured.out

    @patch("dicominfo.viewer.display_images")
    @patch("dicominfo.cli.print_stats")
    @patch("sys.argv", ["dicom-info", "-d", "mock_file.dcm"])
    def test_exits_with_code_1_on_no_pixel_data_error(
        self,
        mock_print_stats: Callable,  # noqa: ARG002
        mock_display_images: Callable,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        """Test that main exits with code 1 when NoPixelDataError is raised."""
        from dicominfo import main
        from dicominfo.exceptions import NoPixelDataError

        # Mock display_images to raise NoPixelDataError
        mock_display_images.side_effect = NoPixelDataError("No pixel data")

        with pytest.raises(SystemExit) as exc_info:
            main()

        assert exc_info.value.code == 1
        captured = capsys.readouterr()
        assert "No pixel data" in captured.out

    @patch("sys.argv", ["dicom-info", "--version"])
    def test_version_flag_exits_with_code_0(
        self, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """Test that --version flag displays version and exits."""
        from dicominfo import __version__, main

        with pytest.raises(SystemExit) as exc_info:
            main()

        assert exc_info.value.code == 0
        captured = capsys.readouterr()
        assert __version__ in captured.out

    @patch("dicominfo.cli.print_stats")
    @patch("sys.argv", ["dicom-info", "-v", "mock_file.dcm"])
    def test_verbose_flag_enables_debug_logging(
        self, mock_print_stats: Callable, caplog: pytest.CaptureFixture[str]
    ) -> None:
        """Test that -v/--verbose flag enables debug logging."""
        import logging

        from dicominfo import main

        with caplog.at_level(logging.DEBUG):
            main()

        # Check that basicConfig was called with DEBUG level
        # We can't directly test the level, but we can verify the function ran
        mock_print_stats.assert_called_once()

    @patch("dicominfo.core.validate_files")
    @patch("sys.argv", ["dicom-info", "-q", "mock_file.dcm"])
    def test_quiet_flag_suppresses_output(
        self, mock_validate_files: Callable, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """Test that -q/--quiet flag suppresses metadata output."""
        from dicominfo import main

        main()

        # validate_files should be called instead of print_stats
        mock_validate_files.assert_called_once()

        # No output should be printed
        captured = capsys.readouterr()
        assert captured.out == ""

    @patch("dicominfo.cli.print_stats")
    @patch("sys.argv", ["dicom-info", "--verbose", "mock_file.dcm"])
    def test_verbose_long_flag_enables_debug_logging(
        self, mock_print_stats: Callable, caplog: pytest.CaptureFixture[str]
    ) -> None:
        """Test that --verbose flag enables debug logging."""
        import logging

        from dicominfo import main

        with caplog.at_level(logging.DEBUG):
            main()

        # Check that basicConfig was called with DEBUG level
        # We can't directly test the level, but we can verify the function ran
        mock_print_stats.assert_called_once()

    @patch("dicominfo.core.validate_files")
    @patch("sys.argv", ["dicom-info", "--quiet", "mock_file.dcm"])
    def test_quiet_long_flag_suppresses_output(
        self, mock_validate_files: Callable
    ) -> None:
        """Test that --quiet flag suppresses metadata output."""
        from dicominfo import main

        main()

        # validate_files should be called instead of print_stats
        mock_validate_files.assert_called_once()

    @patch("sys.argv", ["dicom-info", "--display", "-c", "0", "mock_file.dcm"])
    def test_zero_columns_shows_validation_error(
        self, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """Test that --columns=0 shows validation error and exits with code 2."""
        from dicominfo import main

        with pytest.raises(SystemExit) as exc_info:
            main()

        assert exc_info.value.code == 2
        captured = capsys.readouterr()
        assert "--columns must be a positive integer" in captured.err

    @patch(
        "sys.argv", ["dicom-info", "--display", "-c", "-1", "mock_file.dcm"]
    )
    def test_negative_columns_shows_validation_error(
        self, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """Test that --columns=-1 shows validation error and exits with code 2."""
        from dicominfo import main

        with pytest.raises(SystemExit) as exc_info:
            main()

        assert exc_info.value.code == 2
        captured = capsys.readouterr()
        assert "--columns must be a positive integer" in captured.err

    @patch("dicominfo.cli.print_stats")
    @patch("sys.argv", ["dicom-info", "-c", "0", "mock_file.dcm"])
    def test_invalid_columns_with_no_display_is_ok(
        self,
        mock_print_stats: Callable,
        caplog: pytest.CaptureFixture[str],
    ) -> None:
        """Test that --columns=0 is valid if no --display flag is passed."""
        from dicominfo import main

        main()

        mock_print_stats.assert_called_once()

        assert "only applies to the --display flag" in caplog.text

    @patch("dicominfo.viewer.display_images")
    @patch("dicominfo.cli.print_stats")
    @patch("sys.argv", ["dicom-info", "--display", "-c", "1", "mock_file.dcm"])
    def test_positive_columns_is_valid(
        self,
        mock_print_stats: Callable,
        mock_display_images: Callable,  # noqa: ARG002
    ) -> None:
        """Test that --columns=1 is accepted as valid."""
        from dicominfo import main

        main()

        # Should not raise SystemExit for valid columns value
        mock_print_stats.assert_called_once()

    @patch("dicominfo.viewer.display_images")
    @patch("dicominfo.cli.print_stats")
    @patch("sys.argv", ["dicom-info", "-d", "-c", "5", "mock_file.dcm"])
    def test_positive_columns_passed_to_display_images(
        self,
        mock_print_stats: Callable,  # noqa: ARG002
        mock_display_images: Callable,
    ) -> None:
        """Test that valid --columns value is passed to display_images correctly."""
        from dicominfo import main

        main()

        # Verify display_images was called with the correct columns value
        mock_display_images.assert_called_once_with(
            ["mock_file.dcm"], max_cols=5
        )


class TestGetImageType:
    """Tests for _get_image_type helper function."""

    def test_2d_grayscale_image(self) -> None:
        """Test that 2D grayscale images are correctly identified."""
        mock_dcm = MagicMock()
        mock_dcm.SamplesPerPixel = 1
        pixel_array = np.zeros((256, 256), dtype=np.uint16)

        result = _get_image_type(mock_dcm, pixel_array)

        assert result == "2d_gray"

    def test_2d_rgb_image(self) -> None:
        """Test that 2D RGB images are correctly identified."""
        mock_dcm = MagicMock()
        mock_dcm.SamplesPerPixel = 3
        pixel_array = np.zeros((256, 256, 3), dtype=np.uint8)

        result = _get_image_type(mock_dcm, pixel_array)

        assert result == "2d_rgb"

    def test_2d_rgba_image(self) -> None:
        """Test that 2D RGBA images (4 channels) are correctly identified."""
        mock_dcm = MagicMock()
        mock_dcm.SamplesPerPixel = 4
        pixel_array = np.zeros((256, 256, 4), dtype=np.uint8)

        result = _get_image_type(mock_dcm, pixel_array)

        assert result == "2d_rgb"

    def test_3d_volume_without_number_of_frames(self) -> None:
        """Test that 3D volume data is correctly identified."""
        mock_dcm = MagicMock()
        mock_dcm.SamplesPerPixel = 1
        del mock_dcm.NumberOfFrames  # No NumberOfFrames attribute
        pixel_array = np.zeros((10, 256, 256), dtype=np.uint16)

        result = _get_image_type(mock_dcm, pixel_array)

        assert result == "3d_volume"

    def test_3d_volume_with_number_of_frames(self) -> None:
        """Test that 3D volume with NumberOfFrames is correctly identified."""
        mock_dcm = MagicMock()
        mock_dcm.SamplesPerPixel = 1
        mock_dcm.NumberOfFrames = 10
        pixel_array = np.zeros((10, 256, 256), dtype=np.uint16)

        result = _get_image_type(mock_dcm, pixel_array)

        assert result == "3d_volume"

    def test_warns_on_number_of_frames_mismatch(
        self, caplog: pytest.CaptureFixture[str]
    ) -> None:
        """Test that warning is logged.

        When NumberOfFrames doesn't match shape.
        """
        mock_dcm = MagicMock()
        mock_dcm.SamplesPerPixel = 1
        mock_dcm.NumberOfFrames = 15
        pixel_array = np.zeros((10, 256, 256), dtype=np.uint16)

        result = _get_image_type(mock_dcm, pixel_array)

        assert result == "3d_volume"
        assert "NumberOfFrames" in caplog.text
        assert "doesn't match" in caplog.text

    def test_unsupported_4d_array(self) -> None:
        """Test that 4D arrays are marked as unsupported."""
        mock_dcm = MagicMock()
        mock_dcm.SamplesPerPixel = 1
        pixel_array = np.zeros((10, 10, 256, 256), dtype=np.uint16)

        result = _get_image_type(mock_dcm, pixel_array)

        assert result == "unsupported"

    def test_unsupported_rgb_with_wrong_shape(
        self, caplog: pytest.CaptureFixture[str]
    ) -> None:
        """Test that RGB images with unexpected shapes are unsupported."""
        mock_dcm = MagicMock()
        mock_dcm.SamplesPerPixel = 3
        pixel_array = np.zeros((256, 256), dtype=np.uint8)  # Should be 3D

        result = _get_image_type(mock_dcm, pixel_array)

        assert result == "unsupported"
        assert "Unexpected shape" in caplog.text

    def test_defaults_samples_per_pixel_to_1(self) -> None:
        """Test that missing SamplesPerPixel defaults to 1."""
        mock_dcm = MagicMock()
        del mock_dcm.SamplesPerPixel  # Remove attribute
        pixel_array = np.zeros((256, 256), dtype=np.uint16)

        result = _get_image_type(mock_dcm, pixel_array)

        assert result == "2d_gray"


class TestDisplayImagesIntegration:
    """Integration tests for display_images with different DICOM types."""

    @patch("dicominfo.viewer.plt.show")
    @patch("dicominfo.utils.pydicom.dcmread")
    def test_displays_2d_grayscale_correctly(
        self, mock_dcmread: Callable, mock_show: Callable
    ) -> None:
        """Test that 2D grayscale images are displayed correctly."""
        mock_dcm = MagicMock()
        mock_dcm.SamplesPerPixel = 1
        mock_dcm.pixel_array = np.zeros((256, 256), dtype=np.uint16)
        mock_dcmread.return_value = mock_dcm

        display_images(["test.dcm"])

        # Should call show once
        mock_show.assert_called_once()

    @patch("dicominfo.viewer.plt.show")
    @patch("dicominfo.utils.pydicom.dcmread")
    def test_displays_rgb_without_grayscale_colormap(
        self, mock_dcmread: Callable, mock_show: Callable
    ) -> None:
        """Test that RGB images are not displayed with grayscale colormap."""
        mock_dcm = MagicMock()
        mock_dcm.SamplesPerPixel = 3
        mock_dcm.pixel_array = np.zeros((256, 256, 3), dtype=np.uint8)
        mock_dcmread.return_value = mock_dcm

        # The key is that it doesn't crash trying to slice RGB on axis 0
        display_images(["test_rgb.dcm"])

        # Should call show once
        mock_show.assert_called_once()

    @patch("dicominfo.viewer.plt.show")
    @patch("dicominfo.utils.pydicom.dcmread")
    def test_displays_3d_volume_with_slider(
        self, mock_dcmread: Callable, mock_show: Callable
    ) -> None:
        """Test that 3D volumes are displayed with a slider."""
        mock_dcm = MagicMock()
        mock_dcm.SamplesPerPixel = 1
        mock_dcm.NumberOfFrames = 10
        mock_dcm.pixel_array = np.zeros((10, 256, 256), dtype=np.uint16)
        mock_dcmread.return_value = mock_dcm

        display_images(["test_3d.dcm"])

        # Should call show once
        mock_show.assert_called_once()

    @patch("dicominfo.viewer.plt.show")
    @patch("dicominfo.utils.pydicom.dcmread")
    def test_handles_multi_frame_temporal_data(
        self, mock_dcmread: Callable, mock_show: Callable
    ) -> None:
        """Test that multi-frame temporal data is displayed with slider."""
        mock_dcm = MagicMock()
        mock_dcm.SamplesPerPixel = 1
        mock_dcm.NumberOfFrames = 20
        mock_dcm.pixel_array = np.zeros((20, 128, 128), dtype=np.uint16)
        mock_dcmread.return_value = mock_dcm

        display_images(["test_temporal.dcm"])

        # Should call show once
        mock_show.assert_called_once()


class TestWithPydicomExamples:
    """Tests using real pydicom example datasets instead of mocks."""

    def test_load_dicom_files_with_ct_example(self) -> None:
        """Test loading the CT example from pydicom."""
        from pydicom import examples

        ct_path = str(examples.get_path("ct"))
        result = load_dicom_files([ct_path])

        assert len(result) == 1
        assert hasattr(result[0], "pixel_array")
        assert result[0].Modality == "CT"

    def test_load_multiple_example_files(self) -> None:
        """Test loading multiple real example files."""
        from pydicom import examples

        paths = [str(examples.get_path("ct")), str(examples.get_path("mr"))]
        result = load_dicom_files(paths)

        assert len(result) == 2
        assert result[0].Modality == "CT"
        assert result[1].Modality == "MR"

    def test_validate_files_with_real_examples(self) -> None:
        """Test validation with real example files - should not raise."""
        from pydicom import examples

        paths = [
            str(examples.get_path("ct")),
            str(examples.get_path("mr")),
            str(examples.get_path("rgb_color")),
        ]
        # Should complete without raising DicomReadError
        validate_files(paths)

    def test_get_image_type_ct_is_2d_gray(self) -> None:
        """Test that CT example is identified as 2D grayscale."""
        from pydicom import examples

        dcm = examples.ct
        image_type = _get_image_type(dcm, dcm.pixel_array)

        # CT_small.dcm is a single 2D slice
        assert image_type == "2d_gray"
        assert dcm.SamplesPerPixel == 1

    def test_get_image_type_mr_is_2d_gray(self) -> None:
        """Test that MR example is identified as 2D grayscale."""
        from pydicom import examples

        dcm = examples.mr
        image_type = _get_image_type(dcm, dcm.pixel_array)

        assert image_type == "2d_gray"
        assert len(dcm.pixel_array.shape) == 2

    def test_get_image_type_rgb_color(self) -> None:
        """Test that RGB color example is identified correctly."""
        from pydicom import examples

        dcm = examples.rgb_color
        image_type = _get_image_type(dcm, dcm.pixel_array)

        assert image_type == "2d_rgb"
        assert dcm.SamplesPerPixel == 3
        assert len(dcm.pixel_array.shape) == 3
        assert dcm.pixel_array.shape[2] == 3

    def test_get_image_type_ybr_multi_frame(self) -> None:
        """Test that YBR multi-frame is identified as 3D volume."""
        from pydicom import examples

        dcm = examples.ybr_color
        image_type = _get_image_type(dcm, dcm.pixel_array)

        # YBR color is a multi-frame image RGB image
        # which is currently unsupported
        assert image_type == "unsupported"
        assert len(dcm.pixel_array.shape) == 4

    def test_get_image_type_palette_color(self) -> None:
        """Test palette color image handling."""
        from pydicom import examples

        dcm = examples.palette_color
        image_type = _get_image_type(dcm, dcm.pixel_array)

        # Palette color typically has samples_per_pixel=1 but color map
        # Should be identified based on actual pixel array shape
        assert image_type in ("2d_gray", "3d_volume")

    def test_print_stats_with_ct_example(
        self, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """Test print_stats output with real CT data."""
        from pydicom import examples

        ct_path = str(examples.get_path("ct"))
        print_stats([ct_path])

        captured = capsys.readouterr()
        output = captured.out

        # Check that expected metadata appears in output
        assert "CT" in output or "Patient" in output
        assert "CompressedSamples" in output or "CT1" in output

    def test_print_stats_multiple_files(
        self, capsys: pytest.CaptureFixture[str]
    ) -> None:
        """Test print_stats with multiple real files."""
        from pydicom import examples

        paths = [str(examples.get_path("ct")), str(examples.get_path("mr"))]
        print_stats(paths)

        captured = capsys.readouterr()
        # Should have information about both files
        assert len(captured.out) > 0

    @patch("dicominfo.viewer.plt.show")
    def test_display_ct_image(self, mock_show: Callable) -> None:
        """Test displaying CT example with real pixel data."""
        from pydicom import examples

        ct_path = str(examples.get_path("ct"))
        display_images([ct_path])

        mock_show.assert_called_once()

    @patch("dicominfo.viewer.plt.show")
    def test_display_rgb_color_image(self, mock_show: Callable) -> None:
        """Test displaying RGB color example."""
        from pydicom import examples

        rgb_path = str(examples.get_path("rgb_color"))
        display_images([rgb_path])

        mock_show.assert_called_once()

    @patch("dicominfo.viewer.plt.show")
    def test_display_multiple_real_images(self, mock_show: Callable) -> None:
        """Test displaying multiple different real images."""
        from pydicom import examples

        paths = [
            str(examples.get_path("ct")),
            str(examples.get_path("mr")),
            str(examples.get_path("rgb_color")),
        ]
        display_images(paths, max_cols=2)

        mock_show.assert_called_once()

    def test_display_images_skips_waveform_no_pixels(self) -> None:
        """Test that waveform example (no pixel data) is handled gracefully."""
        from pydicom import examples

        waveform_path = str(examples.get_path("waveform"))

        # Waveform files don't have pixel_array, should raise NoPixelDataError
        # if that's the only file, or skip if mixed with images
        with pytest.raises(NoPixelDataError):
            display_images([waveform_path])

    @patch("dicominfo.viewer.plt.show")
    def test_display_mixed_valid_and_waveform(
        self, mock_show: Callable
    ) -> None:
        """Test displaying CT + waveform - only CT should display."""
        from pydicom import examples

        paths = [
            str(examples.get_path("ct")),
            str(examples.get_path("waveform")),
        ]
        # Should only show CT, skip waveform (no pixel data)
        display_images(paths)

        mock_show.assert_called_once()

    def test_jpeg2000_example_loads_correctly(self) -> None:
        """Test that JPEG2K compressed example loads without errors."""
        from pydicom import examples

        jpeg_path = str(examples.get_path("jpeg2k"))
        dcm = load_dicom_files([jpeg_path])[0]

        # Should have pixel_array even if compressed
        assert hasattr(dcm, "pixel_array")
