"""Filesystem hardening, filename and media-signature tests."""

from __future__ import annotations

import os
import time
from collections import namedtuple
from pathlib import Path

import pytest

from app.utils.files import (
    InvalidMediaFileError,
    UnsafePathError,
    build_download_filename,
    check_disk_space,
    cleanup_stale_temp_files,
    ensure_inside,
    fsync_directory,
    human_size,
    probe_image_file,
    probe_media_file,
    random_part_path,
    remove_file,
    safe_join,
    sanitize_filename,
    sniff_media_header,
    utc_filename_stem,
)
from tests.helpers import MP4_HEADER


def test_sanitize_filename_strips_paths_and_control_characters() -> None:
    assert sanitize_filename("../../etc/passwd") == "passwd"
    assert sanitize_filename("..\\..\\evil.mp4") == "evil.mp4"
    assert sanitize_filename("bad\x00name\n.mp4") == "bad name.mp4"
    assert sanitize_filename("  ...  ") == "video"
    assert sanitize_filename("") == "video"


def test_sanitize_filename_handles_windows_reserved_names() -> None:
    assert sanitize_filename("CON") == "_CON"
    assert sanitize_filename("con.txt") == "_con.txt"
    assert sanitize_filename("LPT9.mp4") == "_LPT9.mp4"
    assert sanitize_filename("COM10.mp4") == "COM10.mp4"


def test_sanitize_filename_preserves_unicode_and_bounds_length() -> None:
    value = sanitize_filename("ویدئوی آزمایشی.mp4")
    assert value == "ویدئوی آزمایشی.mp4"
    long_name = "a" * 500 + ".mp4"
    assert len(sanitize_filename(long_name, max_length=20)) <= 20


def test_safe_join_blocks_escape(tmp_path: Path) -> None:
    base = tmp_path / "downloads"
    base.mkdir()
    joined = safe_join(base, "sub", "file.mp4")
    assert joined == (base / "sub" / "file.mp4").resolve()
    with pytest.raises(UnsafePathError):
        safe_join(base, "..", "outside.mp4")
    with pytest.raises(UnsafePathError):
        safe_join(base, "sub", "..", "..", "outside.mp4")


def test_ensure_inside(tmp_path: Path) -> None:
    root = tmp_path / "root"
    inside = root / "a" / "b.mp4"
    inside.parent.mkdir(parents=True)
    inside.write_bytes(b"x")
    assert ensure_inside(inside, root) == inside.resolve()
    with pytest.raises(UnsafePathError):
        ensure_inside(tmp_path / "other.mp4", root)


def test_build_download_filename_is_safe_and_unique(tmp_path: Path) -> None:
    first = build_download_filename("../../My Video?.mp4", ".MP4")
    second = build_download_filename("../../My Video?.mp4", ".mp4")
    assert first.endswith("-My Video_.mp4")
    assert second.endswith("-My Video_.mp4")
    assert first != second
    assert "/" not in first and "\\" not in first
    assert build_download_filename("clip", ".exe").endswith(".mp4")
    assert build_download_filename("", ".webm").endswith(".webm")


@pytest.mark.parametrize(
    ("header", "expected"),
    [
        (MP4_HEADER, ("video/mp4", ".mp4", "mp4")),
        (b"\x00\x00\x00\x14ftypqt  \x00\x00\x00\x00", ("video/quicktime", ".mov", "mov")),
        (b"\x1aE\xdf\xa3" + b"\x00" * 40, ("video/webm", ".webm", "webm")),
        (b"OggS" + b"\x00" * 40, ("video/ogg", ".ogv", "ogg")),
        (b"RIFF\x24\x00\x00\x00AVI " + b"\x00" * 40, ("video/x-msvideo", ".avi", "avi")),
        (b"FLV\x01" + b"\x00" * 40, ("video/x-flv", ".flv", "flv")),
        (b"\x00\x00\x01\xba" + b"\x00" * 40, ("video/mpeg", ".mpg", "mpeg-ps")),
    ],
)
def test_sniff_supported_containers(header: bytes, expected: tuple[str, str, str]) -> None:
    assert sniff_media_header(header) == expected


def test_sniff_mpeg_transport_stream() -> None:
    packet = bytearray(b"\x00" * 376)
    packet[0] = 0x47
    packet[188] = 0x47
    assert sniff_media_header(bytes(packet)) == ("video/mp2t", ".ts", "mpeg-ts")


@pytest.mark.parametrize(
    "header",
    [
        b"<!doctype html><html><body>not a video</body></html>",
        b"<html><head></head></html>",
        b"<?xml version='1.0'?><root/>",
        b"MZ" + b"\x00" * 40,
        b"\x7fELF" + b"\x00" * 40,
        b"\xca\xfe\xba\xbe" + b"\x00" * 20,
        b"#!" + b"/bin/sh\n",
        b"\x00asm" + b"\x00" * 20,
        b"ID3\x04\x00" + b"\x00" * 40,
        b"\x00\x00\x00\x18ftypM4A " + b"\x00" * 20,
        b"\x00\x00\x00\x18ftypM4P " + b"\x00" * 20,
        b"random text",
        b"",
    ],
)
def test_sniff_rejects_html_executables_audio_and_unknown(header: bytes) -> None:
    assert sniff_media_header(header) is None


def test_probe_media_file_accepts_real_mp4(tmp_path: Path) -> None:
    path = tmp_path / "sample.mp4"
    path.write_bytes(MP4_HEADER + b"\x00" * 1024)
    probe = probe_media_file(path, max_size_bytes=10_000)
    assert probe.mime_type == "video/mp4"
    assert probe.extension == ".mp4"
    assert probe.container == "mp4"
    assert probe.size == path.stat().st_size


def test_probe_media_file_rejects_bad_files(tmp_path: Path) -> None:
    empty = tmp_path / "empty.mp4"
    empty.write_bytes(b"")
    with pytest.raises(InvalidMediaFileError):
        probe_media_file(empty)

    html = tmp_path / "fake.mp4"
    html.write_bytes(b"<html><body>blocked</body></html>")
    with pytest.raises(InvalidMediaFileError):
        probe_media_file(html)

    big = tmp_path / "big.mp4"
    big.write_bytes(MP4_HEADER + b"\x00" * 100)
    with pytest.raises(InvalidMediaFileError):
        probe_media_file(big, max_size_bytes=20)

    with pytest.raises(InvalidMediaFileError):
        probe_media_file(tmp_path / "missing.mp4")


def test_probe_image_file_signatures(tmp_path: Path) -> None:
    jpeg = tmp_path / "a.jpg"
    jpeg.write_bytes(b"\xff\xd8\xff\xe0" + b"\x00" * 100)
    assert probe_image_file(jpeg) == "image/jpeg"

    png = tmp_path / "a.png"
    png.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 100)
    assert probe_image_file(png) == "image/png"

    webp = tmp_path / "a.webp"
    webp.write_bytes(b"RIFF\x00\x00\x00\x00WEBP" + b"\x00" * 100)
    assert probe_image_file(webp) == "image/webp"

    bad = tmp_path / "a.gif"
    bad.write_bytes(b"GIF89a")
    with pytest.raises(InvalidMediaFileError):
        probe_image_file(bad)

    huge = tmp_path / "huge.jpg"
    huge.write_bytes(b"\xff\xd8\xff" + b"x" * 10)
    with pytest.raises(InvalidMediaFileError):
        probe_image_file(huge, max_size_bytes=5)


def test_human_size() -> None:
    assert human_size(None) == "نامشخص"
    assert human_size(0) == "0 B"
    assert human_size(500) == "500 B"
    assert human_size(1024) == "1.0 KB"
    assert human_size(1536) == "1.5 KB"
    assert human_size(1024**3) == "1.0 GB"
    assert human_size(-1) == "0 B"


def test_disk_space_check(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    usage = namedtuple("usage", "total used free")
    monkeypatch.setattr(
        "app.utils.files.shutil.disk_usage",
        lambda path: usage(total=10_000, used=0, free=9_000),
    )
    check_disk_space(tmp_path / "new", 100, reserve_bytes=200)
    with pytest.raises(OSError):
        check_disk_space(tmp_path / "new", 8_900, reserve_bytes=200)


def test_cleanup_stale_temp_files(tmp_path: Path) -> None:
    old = tmp_path / "old.part"
    old.write_bytes(b"old")
    fresh = tmp_path / "fresh.part"
    fresh.write_bytes(b"new")
    old_time = time.time() - 7200
    os.utime(old, (old_time, old_time))
    removed = cleanup_stale_temp_files(tmp_path, max_age_seconds=3600)
    assert removed == 1
    assert not old.exists()
    assert fresh.exists()
    assert cleanup_stale_temp_files(tmp_path / "missing") == 0


def test_remove_file_and_random_part_path(tmp_path: Path) -> None:
    path = tmp_path / "x.part"
    path.write_bytes(b"x")
    remove_file(path)
    assert not path.exists()
    remove_file(None)
    remove_file(path)
    part = random_part_path(tmp_path)
    assert part.parent == tmp_path.resolve()
    assert part.suffix == ".part"
    assert len(utc_filename_stem()) == 32


def test_fsync_directory_is_best_effort(tmp_path: Path) -> None:
    fsync_directory(tmp_path)
