"""
test_youtube_transcribe.py

scripts/youtube-transcribe.py 단위 테스트 (TDD)

테스트 항목:
1. argparse: 필수/선택 인자 파싱 정상 동작
2. download_audio: yt-dlp 호출 및 WAV 파일 반환
3. transcribe_audio: HTTP multipart/form-data 전송 정상 처리
4. transcribe_audio: 연결 실패 시 fallback 경고 반환
5. transcribe_audio: 타임아웃 시 fallback 경고 반환
6. format_output: text/json/srt 형식 변환
7. main: 정상 흐름 통합 (mock)
8. main: 로컬 서비스 불가 시 fallback 메시지
9. main: exit code 성공 0
10. main: exit code 실패 1 (yt-dlp 에러)
"""

import importlib.util
import json
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

# scripts 디렉토리를 import path에 추가
_SCRIPTS_DIR = Path(__file__).parent.parent
sys.path.insert(0, str(_SCRIPTS_DIR))

# youtube-transcribe.py 는 하이픈이 있으므로 importlib으로 임포트
_MODULE_PATH = _SCRIPTS_DIR / "youtube-transcribe.py"
spec = importlib.util.spec_from_file_location("youtube_transcribe", _MODULE_PATH)
assert spec is not None
youtube_transcribe = importlib.util.module_from_spec(spec)
assert spec.loader is not None
# patch()가 sys.modules["youtube_transcribe"]를 통해 접근하므로 먼저 등록
sys.modules["youtube_transcribe"] = youtube_transcribe
spec.loader.exec_module(youtube_transcribe)


# ---------------------------------------------------------------------------
# 1. argparse 인자 파싱 테스트
# ---------------------------------------------------------------------------


class TestParseArgs:
    def test_url_required(self):
        """--url 없이 실행 시 SystemExit 발생"""
        with pytest.raises(SystemExit):
            youtube_transcribe.parse_args([])

    def test_url_only(self):
        """--url만 주면 기본값으로 나머지 채워짐"""
        args = youtube_transcribe.parse_args(["--url", "https://youtube.com/watch?v=abc"])
        assert args.url == "https://youtube.com/watch?v=abc"
        assert args.format == "text"
        assert args.output is None
        assert args.language == "ko"

    def test_all_args(self):
        """모든 인자 지정 시 올바르게 파싱"""
        args = youtube_transcribe.parse_args(
            [
                "--url",
                "https://youtube.com/watch?v=abc",
                "--format",
                "json",
                "--output",
                "/tmp/out.json",
                "--language",
                "en",
            ]
        )
        assert args.url == "https://youtube.com/watch?v=abc"
        assert args.format == "json"
        assert args.output == "/tmp/out.json"
        assert args.language == "en"

    def test_format_choices(self):
        """format은 text|json|srt만 허용"""
        with pytest.raises(SystemExit):
            youtube_transcribe.parse_args(["--url", "https://youtube.com/watch?v=abc", "--format", "xml"])

    def test_format_srt(self):
        """srt 포맷 파싱"""
        args = youtube_transcribe.parse_args(
            ["--url", "https://youtube.com/watch?v=abc", "--format", "srt"]
        )
        assert args.format == "srt"


# ---------------------------------------------------------------------------
# 2. download_audio 테스트
# ---------------------------------------------------------------------------


class TestDownloadAudio:
    def test_calls_yt_dlp(self, tmp_path):
        """yt-dlp YoutubeDL 호출 및 WAV 파일 경로 반환"""
        fake_wav = tmp_path / "audio.wav"
        fake_wav.write_bytes(b"RIFF")

        with patch("youtube_transcribe.yt_dlp.YoutubeDL") as mock_ydl_cls:
            mock_ydl = MagicMock()
            mock_ydl_cls.return_value.__enter__ = MagicMock(return_value=mock_ydl)
            mock_ydl_cls.return_value.__exit__ = MagicMock(return_value=False)

            # download_audio가 반환하는 경로를 fake_wav로 유도하기 위해
            # prepare_filename 모킹
            mock_ydl.prepare_filename.return_value = str(tmp_path / "audio.webm")

            # glob으로 wav 파일 탐색 → fake_wav 존재
            result = youtube_transcribe.download_audio(
                "https://youtube.com/watch?v=abc", str(tmp_path)
            )

        # 반환값은 str 타입이어야 함
        assert isinstance(result, str)

    def test_returns_wav_path(self, tmp_path):
        """download_audio 반환값이 .wav 확장자"""
        fake_wav = tmp_path / "video_title.wav"
        fake_wav.write_bytes(b"RIFF")

        with patch("youtube_transcribe.yt_dlp.YoutubeDL") as mock_ydl_cls:
            mock_ydl = MagicMock()
            ctx_mgr = mock_ydl_cls.return_value
            ctx_mgr.__enter__ = MagicMock(return_value=mock_ydl)
            ctx_mgr.__exit__ = MagicMock(return_value=False)
            mock_ydl.prepare_filename.return_value = str(tmp_path / "video_title.webm")

            result = youtube_transcribe.download_audio(
                "https://youtube.com/watch?v=abc", str(tmp_path)
            )

        assert result.endswith(".wav")

    def test_raises_on_yt_dlp_error(self, tmp_path):
        """yt-dlp 다운로드 실패 시 예외 발생"""
        with patch("youtube_transcribe.yt_dlp.YoutubeDL") as mock_ydl_cls:
            mock_ydl = MagicMock()
            ctx_mgr = mock_ydl_cls.return_value
            ctx_mgr.__enter__ = MagicMock(return_value=mock_ydl)
            ctx_mgr.__exit__ = MagicMock(return_value=False)
            mock_ydl.download.side_effect = Exception("yt-dlp error")

            with pytest.raises(Exception, match="yt-dlp error"):
                youtube_transcribe.download_audio(
                    "https://youtube.com/watch?v=abc", str(tmp_path)
                )


# ---------------------------------------------------------------------------
# 3. transcribe_audio 정상 처리 테스트
# ---------------------------------------------------------------------------


class TestTranscribeAudioSuccess:
    def test_returns_transcription_result(self, tmp_path):
        """정상 응답 시 dict 반환"""
        fake_wav = tmp_path / "audio.wav"
        fake_wav.write_bytes(b"RIFF")

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "text": "안녕하세요",
            "segments": [{"start": 0.0, "end": 1.5, "text": "안녕하세요"}],
        }

        with patch("youtube_transcribe.requests.post", return_value=mock_response):
            result = youtube_transcribe.transcribe_audio(str(fake_wav), language="ko")

        assert result["text"] == "안녕하세요"
        assert "segments" in result

    def test_sends_multipart_form_data(self, tmp_path):
        """requests.post에 files 인자(multipart) 전달 확인"""
        fake_wav = tmp_path / "audio.wav"
        fake_wav.write_bytes(b"RIFF")

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"text": "test"}

        with patch("youtube_transcribe.requests.post", return_value=mock_response) as mock_post:
            youtube_transcribe.transcribe_audio(str(fake_wav), language="ko")

        call_kwargs = mock_post.call_args
        # files 인자가 전달되어야 함 (multipart/form-data)
        assert call_kwargs.kwargs.get("files") is not None or (
            len(call_kwargs.args) > 1 and "files" in str(call_kwargs)
        )

    def test_sends_correct_url(self, tmp_path):
        """로컬 Whisper GPU 서비스 URL로 POST 전송"""
        fake_wav = tmp_path / "audio.wav"
        fake_wav.write_bytes(b"RIFF")

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"text": "test"}

        with patch("youtube_transcribe.requests.post", return_value=mock_response) as mock_post:
            youtube_transcribe.transcribe_audio(str(fake_wav), language="ko")

        call_args = mock_post.call_args
        url_arg = call_args.args[0] if call_args.args else call_args.kwargs.get("url", "")
        assert "localhost:8200" in url_arg
        assert "/v1/transcribe" in url_arg

    def test_timeout_600_seconds(self, tmp_path):
        """timeout=600 으로 POST 전송"""
        fake_wav = tmp_path / "audio.wav"
        fake_wav.write_bytes(b"RIFF")

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"text": "test"}

        with patch("youtube_transcribe.requests.post", return_value=mock_response) as mock_post:
            youtube_transcribe.transcribe_audio(str(fake_wav), language="ko")

        call_kwargs = mock_post.call_args.kwargs
        assert call_kwargs.get("timeout") == 600


# ---------------------------------------------------------------------------
# 4 & 5. transcribe_audio fallback 테스트
# ---------------------------------------------------------------------------


class TestTranscribeAudioFallback:
    def test_connection_error_returns_fallback(self, tmp_path, capsys):
        """ConnectionError 시 fallback dict 반환 + 경고 로그"""
        import requests as req_module

        fake_wav = tmp_path / "audio.wav"
        fake_wav.write_bytes(b"RIFF")

        with patch(
            "youtube_transcribe.requests.post",
            side_effect=req_module.exceptions.ConnectionError("refused"),
        ):
            result = youtube_transcribe.transcribe_audio(str(fake_wav), language="ko")

        assert result["text"] == "로컬 Whisper 서비스가 응답하지 않습니다"
        captured = capsys.readouterr()
        assert "경고" in captured.err or "WARNING" in captured.err or "Whisper" in captured.err

    def test_timeout_error_returns_fallback(self, tmp_path, capsys):
        """Timeout 시 fallback dict 반환 + 경고 로그"""
        import requests as req_module

        fake_wav = tmp_path / "audio.wav"
        fake_wav.write_bytes(b"RIFF")

        with patch(
            "youtube_transcribe.requests.post",
            side_effect=req_module.exceptions.Timeout("timeout"),
        ):
            result = youtube_transcribe.transcribe_audio(str(fake_wav), language="ko")

        assert result["text"] == "로컬 Whisper 서비스가 응답하지 않습니다"
        captured = capsys.readouterr()
        assert "경고" in captured.err or "WARNING" in captured.err or "Whisper" in captured.err

    def test_fallback_has_no_segments(self, tmp_path):
        """fallback 결과에 segments 키 없거나 빈 리스트"""
        import requests as req_module

        fake_wav = tmp_path / "audio.wav"
        fake_wav.write_bytes(b"RIFF")

        with patch(
            "youtube_transcribe.requests.post",
            side_effect=req_module.exceptions.ConnectionError("refused"),
        ):
            result = youtube_transcribe.transcribe_audio(str(fake_wav), language="ko")

        segments = result.get("segments", [])
        assert segments == [] or segments is None


# ---------------------------------------------------------------------------
# 6. format_output 테스트
# ---------------------------------------------------------------------------


class TestFormatOutput:
    SAMPLE_RESULT = {
        "text": "안녕하세요 반갑습니다",
        "segments": [
            {"start": 0.0, "end": 1.5, "text": "안녕하세요"},
            {"start": 1.5, "end": 3.0, "text": "반갑습니다"},
        ],
    }

    def test_text_format(self):
        """text 포맷은 순수 텍스트 반환"""
        output = youtube_transcribe.format_output(self.SAMPLE_RESULT, "text")
        assert "안녕하세요 반갑습니다" in output
        assert isinstance(output, str)

    def test_json_format(self):
        """json 포맷은 파싱 가능한 JSON 문자열 반환"""
        output = youtube_transcribe.format_output(self.SAMPLE_RESULT, "json")
        parsed = json.loads(output)
        assert parsed["text"] == "안녕하세요 반갑습니다"
        assert len(parsed["segments"]) == 2

    def test_srt_format(self):
        """srt 포맷은 SRT 형식 문자열 반환"""
        output = youtube_transcribe.format_output(self.SAMPLE_RESULT, "srt")
        # SRT는 숫자 인덱스 → 타임코드 → 텍스트 구조
        assert "1" in output
        assert "-->" in output
        assert "안녕하세요" in output

    def test_srt_timecode_format(self):
        """srt 타임코드가 HH:MM:SS,mmm --> HH:MM:SS,mmm 형식"""
        output = youtube_transcribe.format_output(self.SAMPLE_RESULT, "srt")
        # 00:00:00,000 --> 00:00:01,500
        assert "00:00:00,000" in output
        assert "00:00:01,500" in output

    def test_text_format_without_segments(self):
        """segments 없는 경우 text 포맷 정상 동작"""
        result = {"text": "테스트"}
        output = youtube_transcribe.format_output(result, "text")
        assert "테스트" in output

    def test_json_format_fallback_message(self):
        """fallback 메시지도 json으로 직렬화 가능"""
        result = {"text": "로컬 Whisper 서비스가 응답하지 않습니다", "segments": []}
        output = youtube_transcribe.format_output(result, "json")
        parsed = json.loads(output)
        assert "Whisper" in parsed["text"]


# ---------------------------------------------------------------------------
# 7. main 통합 테스트 (정상 흐름)
# ---------------------------------------------------------------------------


class TestMainSuccess:
    def _make_mock_result(self):
        return {
            "text": "테스트 전사 결과",
            "segments": [{"start": 0.0, "end": 2.0, "text": "테스트 전사 결과"}],
        }

    def test_main_text_to_stdout(self, capsys):
        """정상 흐름: text 포맷으로 stdout 출력"""
        with patch("youtube_transcribe.download_audio", return_value="/tmp/audio.wav"), patch(
            "youtube_transcribe.transcribe_audio", return_value=self._make_mock_result()
        ), patch("youtube_transcribe.tempfile.mkdtemp", return_value="/tmp/yt_test"), patch(
            "youtube_transcribe.shutil.rmtree"
        ):
            youtube_transcribe.main(
                ["--url", "https://youtube.com/watch?v=abc", "--format", "text"]
            )

        captured = capsys.readouterr()
        assert "테스트 전사 결과" in captured.out

    def test_main_json_to_stdout(self, capsys):
        """json 포맷으로 stdout 출력"""
        with patch("youtube_transcribe.download_audio", return_value="/tmp/audio.wav"), patch(
            "youtube_transcribe.transcribe_audio", return_value=self._make_mock_result()
        ), patch("youtube_transcribe.tempfile.mkdtemp", return_value="/tmp/yt_test"), patch(
            "youtube_transcribe.shutil.rmtree"
        ):
            youtube_transcribe.main(
                ["--url", "https://youtube.com/watch?v=abc", "--format", "json"]
            )

        captured = capsys.readouterr()
        parsed = json.loads(captured.out)
        assert parsed["text"] == "테스트 전사 결과"

    def test_main_output_to_file(self, tmp_path):
        """--output 지정 시 파일에 저장"""
        out_file = tmp_path / "result.txt"

        with patch("youtube_transcribe.download_audio", return_value="/tmp/audio.wav"), patch(
            "youtube_transcribe.transcribe_audio", return_value=self._make_mock_result()
        ), patch("youtube_transcribe.tempfile.mkdtemp", return_value="/tmp/yt_test"), patch(
            "youtube_transcribe.shutil.rmtree"
        ):
            youtube_transcribe.main(
                [
                    "--url",
                    "https://youtube.com/watch?v=abc",
                    "--format",
                    "text",
                    "--output",
                    str(out_file),
                ]
            )

        assert out_file.exists()
        content = out_file.read_text(encoding="utf-8")
        assert "테스트 전사 결과" in content

    def test_main_cleans_temp_dir(self):
        """임시 디렉토리가 정리됨"""
        with patch("youtube_transcribe.download_audio", return_value="/tmp/audio.wav"), patch(
            "youtube_transcribe.transcribe_audio", return_value=self._make_mock_result()
        ), patch(
            "youtube_transcribe.tempfile.mkdtemp", return_value="/tmp/yt_test_cleanup"
        ), patch(
            "youtube_transcribe.shutil.rmtree"
        ) as mock_rmtree:
            youtube_transcribe.main(
                ["--url", "https://youtube.com/watch?v=abc"]
            )

        mock_rmtree.assert_called_once_with("/tmp/yt_test_cleanup", ignore_errors=True)

    def test_main_passes_language_to_transcribe(self):
        """--language 인자가 transcribe_audio에 전달됨"""
        with patch("youtube_transcribe.download_audio", return_value="/tmp/audio.wav"), patch(
            "youtube_transcribe.transcribe_audio", return_value=self._make_mock_result()
        ) as mock_transcribe, patch(
            "youtube_transcribe.tempfile.mkdtemp", return_value="/tmp/yt_test"
        ), patch(
            "youtube_transcribe.shutil.rmtree"
        ):
            youtube_transcribe.main(
                ["--url", "https://youtube.com/watch?v=abc", "--language", "en"]
            )

        mock_transcribe.assert_called_once()
        call_kwargs = mock_transcribe.call_args
        assert call_kwargs.kwargs.get("language") == "en" or (
            len(call_kwargs.args) > 1 and call_kwargs.args[1] == "en"
        )


# ---------------------------------------------------------------------------
# 8. main fallback 메시지 테스트
# ---------------------------------------------------------------------------


class TestMainFallback:
    def test_fallback_message_in_output(self, capsys):
        """서비스 불가 시 fallback 메시지 출력"""
        fallback_result = {
            "text": "로컬 Whisper 서비스가 응답하지 않습니다",
            "segments": [],
        }

        with patch("youtube_transcribe.download_audio", return_value="/tmp/audio.wav"), patch(
            "youtube_transcribe.transcribe_audio", return_value=fallback_result
        ), patch("youtube_transcribe.tempfile.mkdtemp", return_value="/tmp/yt_test"), patch(
            "youtube_transcribe.shutil.rmtree"
        ):
            youtube_transcribe.main(
                ["--url", "https://youtube.com/watch?v=abc", "--format", "text"]
            )

        captured = capsys.readouterr()
        assert "로컬 Whisper 서비스가 응답하지 않습니다" in captured.out


# ---------------------------------------------------------------------------
# 9 & 10. exit code 테스트
# ---------------------------------------------------------------------------


class TestExitCode:
    def test_exit_code_0_on_success(self):
        """성공 시 SystemExit 없이 정상 종료 (exit code 0)"""
        mock_result = {"text": "성공", "segments": []}

        with patch("youtube_transcribe.download_audio", return_value="/tmp/audio.wav"), patch(
            "youtube_transcribe.transcribe_audio", return_value=mock_result
        ), patch("youtube_transcribe.tempfile.mkdtemp", return_value="/tmp/yt_test"), patch(
            "youtube_transcribe.shutil.rmtree"
        ):
            # SystemExit이 발생하지 않아야 함 (exit code 0)
            try:
                youtube_transcribe.main(["--url", "https://youtube.com/watch?v=abc"])
            except SystemExit as e:
                assert e.code == 0, f"Expected exit code 0, got {e.code}"

    def test_exit_code_1_on_download_error(self):
        """yt-dlp 실패 시 SystemExit(1)"""
        with patch(
            "youtube_transcribe.download_audio", side_effect=Exception("yt-dlp failed")
        ), patch("youtube_transcribe.tempfile.mkdtemp", return_value="/tmp/yt_test"), patch(
            "youtube_transcribe.shutil.rmtree"
        ):
            with pytest.raises(SystemExit) as exc_info:
                youtube_transcribe.main(["--url", "https://youtube.com/watch?v=abc"])

        assert exc_info.value.code == 1

    def test_temp_dir_cleaned_even_on_error(self):
        """에러 발생 시에도 임시 디렉토리 정리"""
        with patch(
            "youtube_transcribe.download_audio", side_effect=Exception("download error")
        ), patch(
            "youtube_transcribe.tempfile.mkdtemp", return_value="/tmp/yt_test_err"
        ), patch(
            "youtube_transcribe.shutil.rmtree"
        ) as mock_rmtree:
            with pytest.raises(SystemExit):
                youtube_transcribe.main(["--url", "https://youtube.com/watch?v=abc"])

        mock_rmtree.assert_called_once_with("/tmp/yt_test_err", ignore_errors=True)
