import tempfile
from pathlib import Path
from unittest.mock import AsyncMock, patch

import pytest


class TestFFProbe:
    """Test the ffprobe module functionality."""

    def setup_method(self):
        """Set up test files."""
        self.temp_dir = tempfile.mkdtemp()
        self.test_file = Path(self.temp_dir) / "test_video.mp4"
        self.test_file.touch()

    def teardown_method(self):
        """Clean up test files."""
        import shutil
        shutil.rmtree(self.temp_dir, ignore_errors=True)

    @pytest.mark.asyncio
    async def test_ffprobe_with_existing_file(self):
        """Test ffprobe with an existing file."""
        from app.library.ffprobe import FFProbeResult, ffprobe

        # Mock subprocess to avoid actual ffprobe execution
        with patch("asyncio.create_subprocess_exec") as mock_subprocess:
            # Mock the subprocess result
            mock_process = AsyncMock()
            mock_process.wait.return_value = 0
            mock_process.communicate.return_value = (b'{"format": {"duration": "10.0"}}', b"")
            mock_subprocess.return_value = mock_process

            with patch("anyio.open_file") as mock_open_file:
                mock_file = AsyncMock()
                mock_open_file.return_value.__aenter__.return_value = mock_file

                result = await ffprobe(str(self.test_file))
                assert isinstance(result, FFProbeResult)

    @pytest.mark.asyncio
    async def test_ffprobe_with_nonexistent_file(self):
        """Test ffprobe with a non-existent file."""
        from app.library.ffprobe import ffprobe

        nonexistent_file = Path(self.temp_dir) / "does_not_exist.mp4"

        with pytest.raises(OSError, match="No such media file"):
            await ffprobe(str(nonexistent_file))

    @pytest.mark.asyncio
    async def test_ffprobe_caching_behavior(self):
        """Test that ffprobe results are cached with enhanced async timed_lru_cache."""
        from app.library.ffprobe import ffprobe

        # Test that the function has been decorated with caching
        assert hasattr(ffprobe, "cache_clear"), "ffprobe should have cache_clear method from timed_lru_cache"
        assert hasattr(ffprobe, "cache_info"), "ffprobe should have cache_info method from timed_lru_cache"

        # Clear cache to start fresh
        ffprobe.cache_clear()

        # Mock subprocess to avoid actual ffprobe execution
        call_count = 0
        with patch("asyncio.create_subprocess_exec") as mock_subprocess:
            def create_mock_process(*_args, **_kwargs):
                nonlocal call_count
                call_count += 1
                mock_process = AsyncMock()
                mock_process.wait.return_value = 0
                mock_process.communicate.return_value = (b'{"format": {"duration": "10.0"}, "streams": []}', b"")
                return mock_process

            mock_subprocess.side_effect = create_mock_process

            with patch("anyio.open_file") as mock_open_file:
                mock_file = AsyncMock()
                mock_open_file.return_value.__aenter__.return_value = mock_file

                # First call should execute the function
                result1 = await ffprobe(str(self.test_file))
                assert result1 is not None
                assert isinstance(result1.metadata, dict)
                first_call_count = call_count

                # Second call with same argument should use cached result
                result2 = await ffprobe(str(self.test_file))
                assert result2 is not None
                assert isinstance(result2.metadata, dict)

                # The subprocess should not be called again for the actual ffprobe execution
                # (it may be called for the -h check, but the main execution should be cached)
                assert call_count == first_call_count, "Second call should use cached result"

                # Results should be equivalent (same data, may not be same object due to async nature)
                assert result1.metadata == result2.metadata

    @pytest.mark.asyncio
    async def test_ffprobe_with_path_object(self):
        """Test ffprobe with Path object input."""
        from app.library.ffprobe import ffprobe

        # Mock subprocess to avoid actual ffprobe execution
        with patch("asyncio.create_subprocess_exec") as mock_subprocess:
            mock_process = AsyncMock()
            mock_process.wait.return_value = 0
            mock_process.communicate.return_value = (b'{"format": {"duration": "10.0"}}', b"")
            mock_subprocess.return_value = mock_process

            with patch("anyio.open_file") as mock_open_file:
                mock_file = AsyncMock()
                mock_open_file.return_value.__aenter__.return_value = mock_file

                # Test with Path object
                result = await ffprobe(self.test_file)
                assert result is not None

    def test_ffprobe_result_properties(self):
        """Test FFProbeResult object properties."""
        from app.library.ffprobe import FFProbeResult, FFStream

        result = FFProbeResult()

        # Test empty result
        assert result.video == []
        assert result.audio == []
        assert result.subtitle == []
        assert result.attachment == []
        assert result.metadata == {}

        # Test adding streams
        video_stream = FFStream({
            "index": 0,
            "codec_type": "video",
            "codec_name": "h264",
            "width": 1920,
            "height": 1080
        })

        audio_stream = FFStream({
            "index": 1,
            "codec_type": "audio",
            "codec_name": "aac",
            "channels": 2
        })

        result.video.append(video_stream)
        result.audio.append(audio_stream)

        assert len(result.video) == 1
        assert len(result.audio) == 1
        assert result.video[0].is_video()
        assert result.audio[0].is_audio()

    def test_stream_object_methods(self):
        """Test Stream object methods."""
        from app.library.ffprobe import FFStream

        # Test video stream
        video_stream = FFStream({
            "codec_type": "video",
            "codec_name": "h264",
            "width": 1920,
            "height": 1080,
            "duration": "10.5"
        })

        assert video_stream.is_video()
        assert not video_stream.is_audio()
        assert not video_stream.is_subtitle()
        assert not video_stream.is_attachment()

        # Test audio stream
        audio_stream = FFStream({
            "codec_type": "audio",
            "codec_name": "aac",
            "channels": 2,
            "sample_rate": "44100"
        })

        assert not audio_stream.is_video()
        assert audio_stream.is_audio()
        assert not audio_stream.is_subtitle()
        assert not audio_stream.is_attachment()

        # Test subtitle stream
        subtitle_stream = FFStream({
            "codec_type": "subtitle",
            "codec_name": "subrip"
        })

        assert not subtitle_stream.is_video()
        assert not subtitle_stream.is_audio()
        assert subtitle_stream.is_subtitle()
        assert not subtitle_stream.is_attachment()

        # Test attachment stream
        attachment_stream = FFStream({
            "codec_type": "attachment",
            "codec_name": "ttf"
        })

        assert not attachment_stream.is_video()
        assert not attachment_stream.is_audio()
        assert not attachment_stream.is_subtitle()
        assert attachment_stream.is_attachment()
