"""
v5 behavior tests — skip-invalid-files, early-abort, file-type diagnostics.

These tests verify the 3 root-cause fixes from v5:
  - v5-01: Invalid files are SKIPPED, not "attempted anyway"
  - v5-02: 3 consecutive failures auto-abort the queue
  - v5-03: `file` command output in diagnostics reveals HTML/text/data
  - v5-04: Pre-flight validation pass reports valid/invalid counts
"""
import importlib.util
import os
import shutil
import subprocess
import sys
from pathlib import Path
from unittest.mock import MagicMock

import pytest


# ─────────────────────────────────────────────────────────────────────────────
# Module loading — same pattern as the other test files
# ─────────────────────────────────────────────────────────────────────────────
V3_PATH = Path(__file__).resolve().parent.parent / "open-transcode.v3.py"


def _load_v3_module():
    if not V3_PATH.exists():
        pytest.skip(f"v3 source not found at {V3_PATH}")
    _install_pyside6_stubs()
    spec = importlib.util.spec_from_file_location("open_transcode_v3", str(V3_PATH))
    mod = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(mod)
    return mod


def _install_pyside6_stubs():
    if any(name in sys.modules for name in
           ("PySide6", "PySide6.QtWidgets", "PySide6.QtCore", "PySide6.QtGui")):
        return
    import types
    pyside6 = types.ModuleType("PySide6")
    qt_widgets = types.ModuleType("PySide6.QtWidgets")
    qt_core = types.ModuleType("PySide6.QtCore")
    qt_gui = types.ModuleType("PySide6.QtGui")

    class _QThread:
        def __init__(self, *a, **kw): pass
        def start(self): pass
        def isRunning(self): return False
        def wait(self, ms=None): pass

    class _Signal:
        def __init__(self, *a, **kw): pass
        def connect(self, *a, **kw): pass
        def emit(self, *a, **kw): pass

    def _Slot(*a, **kw):
        def deco(fn): return fn
        return deco

    qt_core.QThread = _QThread
    qt_core.Signal = _Signal
    qt_core.Slot = _Slot
    qt_core.Qt = MagicMock()
    qt_core.QPointF = MagicMock()
    qt_core.QRectF = MagicMock()
    qt_core.QTimer = MagicMock()

    class _QWidget:
        def __init__(self, *a, **kw): pass
    class _QMainWindow(_QWidget): pass
    qt_widgets.QWidget = _QWidget
    qt_widgets.QMainWindow = _QMainWindow
    for name in ("QApplication", "QVBoxLayout", "QHBoxLayout", "QLabel",
                 "QLineEdit", "QPushButton", "QComboBox", "QCheckBox",
                 "QTextEdit", "QFileDialog", "QGroupBox", "QStatusBar",
                 "QMessageBox", "QStyleFactory"):
        setattr(qt_widgets, name, MagicMock())

    qt_gui.QFont = MagicMock()
    qt_gui.QPalette = MagicMock()
    qt_gui.QColor = MagicMock()
    qt_gui.QPainter = MagicMock()
    qt_gui.QPen = MagicMock()
    qt_gui.QBrush = MagicMock()
    qt_gui.QRadialGradient = MagicMock()
    qt_gui.QFontMetrics = MagicMock()

    pyside6.QtWidgets = qt_widgets
    pyside6.QtCore = qt_core
    pyside6.QtGui = qt_gui
    sys.modules["PySide6"] = pyside6
    sys.modules["PySide6.QtWidgets"] = qt_widgets
    sys.modules["PySide6.QtCore"] = qt_core
    sys.modules["PySide6.QtGui"] = qt_gui


@pytest.fixture(scope="module")
def v3_module():
    return _load_v3_module()


# ─────────────────────────────────────────────────────────────────────────────
# Tests
# ─────────────────────────────────────────────────────────────────────────────


class TestIdentifyFileType:
    """v5-03: _identify_file_type() runs `file -b` and returns the type string."""

    def test_identifies_text_file(self, v3_module, tmp_path):
        """A .txt file should be identified as 'ASCII text' or similar."""
        f = tmp_path / "test.txt"
        f.write_text("This is not a video file, just plain text.")
        result = v3_module._identify_file_type(f)
        # `file` should identify it as text
        assert "text" in result.lower() or "ascii" in result.lower(), \
            f"Expected text/ASCII in result, got: {result}"

    def test_identifies_html_file(self, v3_module, tmp_path):
        """An HTML file should be identified as 'HTML document' — the classic
        failed yt-dlp download scenario."""
        f = tmp_path / "fake_video.mp4"
        f.write_text("<!DOCTYPE html><html><body>Video unavailable</body></html>")
        result = v3_module._identify_file_type(f)
        assert "HTML" in result or "text" in result.lower(), \
            f"Expected HTML/text in result, got: {result}"

    def test_identifies_real_mp4(self, v3_module, tmp_path, real_ffmpeg=None):
        """A real MP4 should be identified as 'ISO Media' or 'MP4'."""
        if not shutil.which("ffmpeg"):
            pytest.skip("ffmpeg not available")
        f = tmp_path / "real.mp4"
        subprocess.run(
            ["ffmpeg", "-f", "lavfi", "-i", "testsrc=duration=0.1:size=32x32:rate=1",
             "-c:v", "libx264", "-y", str(f)],
            capture_output=True, timeout=10,
        )
        if not f.exists():
            pytest.skip("Could not generate test MP4")
        result = v3_module._identify_file_type(f)
        assert "ISO Media" in result or "MP4" in result or "Media" in result, \
            f"Expected ISO Media/MP4 in result, got: {result}"

    def test_returns_empty_for_nonexistent_file(self, v3_module, tmp_path):
        """Nonexistent file should return empty string (not crash)."""
        f = tmp_path / "does_not_exist.bin"
        result = v3_module._identify_file_type(f)
        # Should not crash; may return empty or an error string
        assert isinstance(result, str)


class TestSkipInvalidFiles:
    """v5-01: Invalid files are SKIPPED, not 'attempted anyway'."""

    def test_validate_file_skips_when_ffprobe_returns_none(
        self, v3_module, tmp_path
    ):
        """When ffprobe can't read a file and force=False, _validate_file
        should return skip=True."""
        # Create a fake "video" file that's actually text
        fake_video = tmp_path / "fake.mp4"
        fake_video.write_text("<!DOCTYPE html><html>Not a video</html>")

        # Build a minimal EncoderWorker via __new__ to bypass __init__
        worker = v3_module.EncoderWorker.__new__(v3_module.EncoderWorker)
        worker.force = False  # v5-01: force=False (default)
        worker.fail_count = 0
        worker._file_res_map = {}
        worker.env = MagicMock()
        worker.env.ffprobe_path = shutil.which("ffprobe") or "/usr/bin/ffprobe"
        logs = []
        worker.log_msg = MagicMock()
        worker.log_msg.emit = lambda msg: logs.append(msg)

        skip, info, src_w, src_h = worker._validate_file(fake_video)

        assert skip is True, "Should skip invalid file when force=False"
        assert info is None
        assert worker.fail_count == 1, "Should increment fail_count"
        # Should mention SKIP in the log
        assert any("SKIP" in l for l in logs), \
            f"Expected SKIP in logs, got: {logs}"
        # Should mention the file type (HTML/text)
        assert any("HTML" in l or "text" in l.lower() for l in logs), \
            f"Expected file type info in logs, got: {logs}"

    def test_validate_file_proceeds_when_force_true(
        self, v3_module, tmp_path
    ):
        """When force=True, _validate_file should NOT skip — it should
        log a WARN and proceed (return skip=False)."""
        fake_video = tmp_path / "fake.mp4"
        fake_video.write_text("<!DOCTYPE html><html>Not a video</html>")

        worker = v3_module.EncoderWorker.__new__(v3_module.EncoderWorker)
        worker.force = True  # v5-01: force=True overrides validation
        worker.fail_count = 0
        worker._file_res_map = {}
        worker.env = MagicMock()
        worker.env.ffprobe_path = shutil.which("ffprobe") or "/usr/bin/ffprobe"
        logs = []
        worker.log_msg = MagicMock()
        worker.log_msg.emit = lambda msg: logs.append(msg)

        skip, info, src_w, src_h = worker._validate_file(fake_video)

        assert skip is False, "Should NOT skip when force=True"
        assert worker.fail_count == 0, "Should NOT increment fail_count"
        # Should log a WARN about attempting anyway
        assert any("WARN" in l and "force" in l.lower() for l in logs), \
            f"Expected WARN about force in logs, got: {logs}"


class TestConsecutiveFailureAbort:
    """v5-02: 3 consecutive failures auto-abort the queue."""

    def test_aborts_after_three_consecutive_failures(self, v3_module):
        """After 3 consecutive failures, _check_consecutive_failures
        should set self._stop = True."""
        worker = v3_module.EncoderWorker.__new__(v3_module.EncoderWorker)
        worker._stop = False
        worker._consecutive_fail_count = 0
        worker._last_fail_pattern = None
        worker.log_msg = MagicMock()
        worker.log_msg.emit = lambda msg: None

        # 3 consecutive failures
        worker._check_consecutive_failures(Path("f1.mp4"), accepted=False)
        assert worker._consecutive_fail_count == 1
        assert worker._stop is False

        worker._check_consecutive_failures(Path("f2.mp4"), accepted=False)
        assert worker._consecutive_fail_count == 2
        assert worker._stop is False

        worker._check_consecutive_failures(Path("f3.mp4"), accepted=False)
        assert worker._consecutive_fail_count == 3
        assert worker._stop is True, "Should auto-abort after 3 consecutive failures"

    def test_success_resets_counter(self, v3_module):
        """A success should reset the consecutive failure counter."""
        worker = v3_module.EncoderWorker.__new__(v3_module.EncoderWorker)
        worker._stop = False
        worker._consecutive_fail_count = 2  # already had 2 failures
        worker._last_fail_pattern = None
        worker.log_msg = MagicMock()
        worker.log_msg.emit = lambda msg: None

        # Success
        worker._check_consecutive_failures(Path("ok.mp4"), accepted=True)
        assert worker._consecutive_fail_count == 0, "Success should reset counter"
        assert worker._stop is False

    def test_does_not_double_abort(self, v3_module):
        """If already stopped (user clicked STOP), don't abort again."""
        worker = v3_module.EncoderWorker.__new__(v3_module.EncoderWorker)
        worker._stop = True  # already stopped
        worker._consecutive_fail_count = 0
        worker._last_fail_pattern = None
        worker.log_msg = MagicMock()
        worker.log_msg.emit = lambda msg: None

        worker._check_consecutive_failures(Path("f.mp4"), accepted=False)
        # Should increment but NOT emit the ABORT message (already stopped)
        assert worker._consecutive_fail_count == 1


class TestErrorPatternsIncludeStreams:
    """v5-03: 'missing field streams' and 'Invalid data found' are in the
    error_patterns table."""

    def test_error_patterns_table_has_streams_pattern(self, v3_module):
        """The error_patterns table in _encode_one should include the
        'missing field streams' pattern. We verify by checking the source
        code (the table is a local variable, not accessible from outside)."""
        # Read the source and check for the pattern
        src = Path(V3_PATH).read_text()
        assert "missing field `streams`" in src, \
            "error_patterns table should include 'missing field streams'"
        assert "Invalid data found when processing input" in src, \
            "error_patterns table should include 'Invalid data found'"

    def test_identify_file_type_called_in_diagnostic(self, v3_module):
        """The diagnostic section should call _identify_file_type for
        the streams/invalid-data patterns."""
        src = Path(V3_PATH).read_text()
        # The _identify_file_type call should be inside the diagnostic block
        assert "_identify_file_type(file_path)" in src, \
            "Diagnostic should call _identify_file_type"


class TestPreFlightValidation:
    """v5-04: Pre-flight validation pass reports valid/invalid counts."""

    def test_run_aborts_when_all_files_invalid(self, v3_module, tmp_path):
        """When ALL files are invalid and force=False, run() should abort
        immediately without entering the encode loop."""
        # Create 3 fake "video" files (actually text)
        for i in range(3):
            (tmp_path / f"fake{i}.mp4").write_text(
                f"<!DOCTYPE html><html>Not a video {i}</html>"
            )

        out_dir = tmp_path / "output"
        out_dir.mkdir()

        av1_codec = next(c for c in v3_module.VIDEO_CODECS if c.label == "AV1 (SVT-AV1)")
        opus_audio = next(a for a in v3_module.AUDIO_PROFILES if a.label == "Opus (96k)")
        mkv_container = next(c for c in v3_module.CONTAINER_PROFILES if c.ext == "mkv")
        original_res = next(r for r in v3_module.RESOLUTION_PRESETS if r.category == "original")

        env = v3_module.probe_environment()

        worker = v3_module.EncoderWorker(
            in_dir=tmp_path,
            out_dir=out_dir,
            video_codec=av1_codec,
            audio_profile=opus_audio,
            container=mkv_container,
            crf=32,
            preset_label="Fast (4)",
            delete_source=False,
            env=env,
            extensions={".mp4"},
            resolution=original_res,
            use_ffmpeg_fallback=True,
            force=False,  # v5-01: default — should skip invalid files
        )

        logs = []
        worker.log_msg.emit = lambda msg: logs.append(msg)

        # Run the worker — should abort in pre-flight validation
        worker.run()

        # Should NOT have entered the encode loop (no "[1/3] Encoding" message)
        assert not any("[1/3] Encoding" in l for l in logs), \
            "Should not enter encode loop when all files are invalid"

        # Should have the PRE-FLIGHT VALIDATION section
        assert any("PRE-FLIGHT VALIDATION" in l for l in logs), \
            f"Expected PRE-FLIGHT VALIDATION in logs"

        # Should have the ABORT message
        assert any("ABORT" in l and "invalid" in l.lower() for l in logs), \
            f"Expected ABORT message about invalid files"

        # Should report 0 valid, 3 invalid
        assert any("Valid files:   0" in l for l in logs)
        assert any("Invalid files: 3" in l for l in logs)
