#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2025 Tony Snearly
# SPDX-License-Identifier: OSL-3.0
"""
Edge case tests for guardian functionality
"""

import os
import tempfile
import unittest
from unittest.mock import MagicMock, mock_open, patch

from guardian.core import GuardianProcessor


class TestGuardianEdgeCases(unittest.TestCase):
    """Edge case test cases for Guardian functionality"""

    def setUp(self):
        """Set up test fixtures"""
        self.processor = GuardianProcessor()
        self.temp_dir = tempfile.mkdtemp()
        self.test_video_path = os.path.join(self.temp_dir, "test.mp4")

    def tearDown(self):
        """Clean up test fixtures"""
        import shutil

        shutil.rmtree(self.temp_dir, ignore_errors=True)

    @patch("subprocess.check_output")
    def test_get_video_details_malformed_audio_stream(self, mock_check_output):
        """Test video details with malformed audio stream data"""
        mock_check_output.side_effect = [
            "120.5",  # duration
            "malformed|audio|data\n|incomplete|",  # malformed audio info
            "1920\n1080\n30000/1001",  # video info
        ]

        result = self.processor.get_video_details(self.test_video_path)

        self.assertIsNotNone(result)
        # Should handle malformed data gracefully
        self.assertEqual(result["duration"], "120.5")

    @patch("subprocess.check_output")
    def test_get_video_details_zero_channels(self, mock_check_output):
        """Test video details with zero channel audio streams"""
        mock_check_output.side_effect = [
            "120.5",  # duration
            "aac|44100|0|none\naac|48000|2|stereo",  # zero channels then valid
            "1920\n1080\n30000/1001",  # video info
        ]

        result = self.processor.get_video_details(self.test_video_path)

        self.assertIsNotNone(result)
        # Should pick the stream with more channels (2 > 0)
        self.assertEqual(result["channels"], "2")
        self.assertEqual(result["audioconfig"], "stereo")

    @patch("subprocess.check_output")
    def test_get_video_details_non_numeric_channels(self, mock_check_output):
        """Test video details with non-numeric channel data"""
        mock_check_output.side_effect = [
            "120.5",  # duration
            "aac|44100|unknown|stereo\naac|48000|2|stereo",  # non-numeric then valid
            "1920\n1080\n30000/1001",  # video info
        ]

        result = self.processor.get_video_details(self.test_video_path)

        self.assertIsNotNone(result)
        # Should skip non-numeric and pick valid stream
        self.assertEqual(result["channels"], "2")

    @patch("subprocess.check_output")
    def test_get_video_details_framerate_division_by_zero(self, mock_check_output):
        """Test video details with zero denominator in framerate"""
        mock_check_output.side_effect = [
            "120.5",  # duration
            "aac|44100|2|stereo",  # audio info
            "1920\n1080\n30000/0",  # video info with zero denominator
        ]

        # This should not crash, but handle the division by zero gracefully
        result = self.processor.get_video_details(self.test_video_path)

        # The function should still return a result, but fps calculation might fail
        self.assertIsNotNone(result)

    @patch("subprocess.check_output")
    @patch("json.loads")
    def test_extract_embedded_srt_unexpected_exception(
        self, mock_json_loads, mock_check_output
    ):
        """Test SRT extraction with unexpected exception"""
        mock_check_output.return_value = '{"streams": []}'
        mock_json_loads.side_effect = RuntimeError("Unexpected error")

        result = self.processor.extract_embedded_srt(
            self.test_video_path, "/tmp/test.srt"
        )

        self.assertFalse(result)

    @patch("subprocess.check_output")
    @patch("json.loads")
    def test_extract_embedded_srt_missing_streams_key(
        self, mock_json_loads, mock_check_output
    ):
        """Test SRT extraction when JSON doesn't have streams key"""
        mock_check_output.return_value = '{"format": {}}'
        mock_json_loads.return_value = {"format": {}}

        result = self.processor.extract_embedded_srt(
            self.test_video_path, "/tmp/test.srt"
        )

        self.assertFalse(result)

    @patch("subprocess.check_output")
    @patch("json.loads")
    def test_extract_embedded_srt_streams_not_list(
        self, mock_json_loads, mock_check_output
    ):
        """Test SRT extraction when streams is not a list"""
        mock_check_output.return_value = '{"streams": "not_a_list"}'
        mock_json_loads.return_value = {"streams": "not_a_list"}

        result = self.processor.extract_embedded_srt(
            self.test_video_path, "/tmp/test.srt"
        )

        self.assertFalse(result)

    @patch("os.path.exists")
    @patch("builtins.open", new_callable=mock_open)
    @patch("srt.parse")
    def test_censor_audio_subtitle_with_special_characters(
        self, mock_srt_parse, mock_file, mock_exists
    ):
        """Test audio censoring with subtitles containing special characters"""
        mock_exists.return_value = True

        # Create subtitle with special characters and profanity
        mock_subtitle = MagicMock()
        mock_subtitle.start.total_seconds.return_value = 10.0
        mock_subtitle.end.total_seconds.return_value = 15.0
        mock_subtitle.content = "What the f*ck!!! [BEEP] $#!t happens..."
        mock_subtitle.index = 1
        mock_srt_parse.return_value = [mock_subtitle]

        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=0, stdout="Success", stderr="")

            result = self.processor.censor_audio_with_ffmpeg(self.test_video_path)

        self.assertIsNotNone(result)

    @patch("os.path.exists")
    @patch("builtins.open", new_callable=mock_open)
    @patch("srt.parse")
    def test_censor_audio_subtitle_case_variations(
        self, mock_srt_parse, mock_file, mock_exists
    ):
        """Test audio censoring with various case patterns in profanity"""
        mock_exists.return_value = True

        subtitles = []
        for i, content in enumerate(
            [
                "FUCKING HELL",  # All caps
                "Shit happens",  # Title case
                "what the HELL",  # Mixed case
                "FuCkInG terrible",  # Alternating case
            ],
            1,
        ):
            mock_subtitle = MagicMock()
            mock_subtitle.start.total_seconds.return_value = i * 10.0
            mock_subtitle.end.total_seconds.return_value = i * 10.0 + 5.0
            mock_subtitle.content = content
            mock_subtitle.index = i
            subtitles.append(mock_subtitle)

        mock_srt_parse.return_value = subtitles

        with patch("subprocess.run") as mock_run, patch.object(
            self.processor, "_verify_silence_level"
        ) as mock_verify:
            mock_run.return_value = MagicMock(returncode=0, stdout="Success", stderr="")
            mock_verify.return_value = (
                True,
                -100.0,
            )  # Mock successful silence verification

            result = self.processor.censor_audio_with_ffmpeg(self.test_video_path)

        self.assertIsNotNone(result)
        # All should be detected due to case-insensitive matching
        call_args = mock_run.call_args[0][0]
        af_index = call_args.index("-af")
        filter_string = call_args[af_index + 1]
        volume_count = filter_string.count("volume=0:enable=")
        self.assertEqual(volume_count, 4)

    @patch("os.path.exists")
    @patch("builtins.open", new_callable=mock_open)
    @patch("srt.parse")
    def test_censor_audio_overlapping_time_segments(
        self, mock_srt_parse, mock_file, mock_exists
    ):
        """Test audio censoring with overlapping time segments"""
        mock_exists.return_value = True

        subtitles = []
        # Create overlapping subtitles with profanity
        for i, (start, end, content) in enumerate(
            [
                (10.0, 15.0, "This is fucking bad"),
                (12.0, 18.0, "Really shit quality"),  # Overlaps with previous
                (16.0, 20.0, "What the hell"),  # Overlaps with previous
            ],
            1,
        ):
            mock_subtitle = MagicMock()
            mock_subtitle.start.total_seconds.return_value = start
            mock_subtitle.end.total_seconds.return_value = end
            mock_subtitle.content = content
            mock_subtitle.index = i
            subtitles.append(mock_subtitle)

        mock_srt_parse.return_value = subtitles

        with patch("subprocess.run") as mock_run, patch.object(
            self.processor, "_verify_silence_level"
        ) as mock_verify:
            mock_run.return_value = MagicMock(returncode=0, stdout="Success", stderr="")
            mock_verify.return_value = (
                True,
                -100.0,
            )  # Mock successful silence verification

            result = self.processor.censor_audio_with_ffmpeg(self.test_video_path)

        self.assertIsNotNone(result)
        # Should handle overlapping segments
        call_args = mock_run.call_args[0][0]
        af_index = call_args.index("-af")
        filter_string = call_args[af_index + 1]
        self.assertIn("volume=0:enable=", filter_string)

    @patch("os.path.exists")
    @patch("builtins.open")
    def test_censor_audio_file_encoding_issues(self, mock_file, mock_exists):
        """Test audio censoring with file encoding issues"""
        mock_exists.return_value = True

        # Mock file open to raise UnicodeDecodeError
        mock_file.side_effect = UnicodeDecodeError(
            "utf-8", b"", 0, 1, "invalid start byte"
        )

        with patch.object(self.processor, "extract_embedded_srt") as mock_extract:
            mock_extract.return_value = False

            result = self.processor.censor_audio_with_ffmpeg(self.test_video_path)

        self.assertIsNone(result)

    def test_process_video_with_get_video_details_failure(self):
        """Test process_video when get_video_details fails"""
        with patch("os.path.exists") as mock_exists, patch.object(
            self.processor, "get_video_details"
        ) as mock_details:

            mock_exists.return_value = True
            mock_details.return_value = None

            result = self.processor.process_video(self.test_video_path)

        self.assertIsNone(result)

    def test_regex_pattern_with_empty_matching_words(self):
        """Test regex pattern compilation with empty matching words list"""
        processor = GuardianProcessor(matching_words=[])

        # Should handle empty list gracefully
        self.assertEqual(processor.matching_words, [])

    def test_regex_pattern_with_special_regex_characters(self):
        """Test regex pattern with words containing special regex characters"""
        special_words = ["test.", "word+", "phrase*", "item?", "group[", "end]"]
        processor = GuardianProcessor(matching_words=special_words)

        # Should escape special characters properly
        import re

        pattern = (
            r"\b("
            + "|".join(re.escape(word) for word in processor.matching_words)
            + r")\b"
        )

        # Should compile without error
        compiled_pattern = re.compile(pattern, re.IGNORECASE)

        # Should match the literal strings, not as regex patterns
        # Note: \b requires word boundaries, punctuation affects word boundaries
        # Let's test with a word that doesn't end in punctuation
        test_words_simple = ["testword", "simpleword"]
        simple_processor = GuardianProcessor(matching_words=test_words_simple)
        simple_pattern = (
            r"\b("
            + "|".join(re.escape(word) for word in simple_processor.matching_words)
            + r")\b"
        )
        simple_compiled = re.compile(simple_pattern, re.IGNORECASE)

        self.assertTrue(simple_compiled.search("testword here"))
        self.assertFalse(
            simple_compiled.search("testwords")
        )  # Should not match partial

    @patch("subprocess.run")
    def test_censor_audio_ffmpeg_file_not_found_error(self, mock_run):
        """Test audio censoring when ffmpeg executable is not found"""
        mock_run.side_effect = FileNotFoundError("ffmpeg not found")

        with patch("os.path.exists") as mock_exists, patch(
            "builtins.open", new_callable=mock_open
        ), patch("srt.parse") as mock_srt_parse:

            mock_exists.return_value = True
            mock_subtitle = MagicMock()
            mock_subtitle.content = (
                "This is fucking bad"  # Use profane content to trigger censoring
            )
            mock_subtitle.start.total_seconds.return_value = 10.0
            mock_subtitle.end.total_seconds.return_value = 15.0
            mock_subtitle.index = 1
            mock_srt_parse.return_value = [mock_subtitle]

            result = self.processor.censor_audio_with_ffmpeg(self.test_video_path)

        self.assertIsNone(result)


if __name__ == "__main__":
    unittest.main()
