"""Tests for core.metadata.art_apply — on-disk art detection + applying art."""

import sys
import types
from types import SimpleNamespace

# Stubs so core.metadata.artwork (pulled in transitively) imports without the
# real Spotify / config dependencies (mirrors test_missing_cover_art.py).
if 'spotipy' not in sys.modules:
    spotipy = types.ModuleType('spotipy')
    oauth2 = types.ModuleType('spotipy.oauth2')
    spotipy.Spotify = type('S', (), {})
    oauth2.SpotifyOAuth = oauth2.SpotifyClientCredentials = type('O', (), {})
    spotipy.oauth2 = oauth2
    sys.modules['spotipy'] = spotipy
    sys.modules['spotipy.oauth2'] = oauth2

if 'config.settings' not in sys.modules:
    config_mod = types.ModuleType('config')
    settings_mod = types.ModuleType('config.settings')

    class _Cfg:
        def get(self, key, default=None):
            return default

        def get_active_media_server(self):
            return 'plex'

    settings_mod.config_manager = _Cfg()
    config_mod.settings = settings_mod
    sys.modules['config'] = config_mod
    sys.modules['config.settings'] = settings_mod

from core.metadata import art_apply as aa


# ── sidecar detection ──

def test_folder_has_cover_sidecar(tmp_path):
    assert aa.folder_has_cover_sidecar(str(tmp_path)) is False
    (tmp_path / 'cover.jpg').write_bytes(b'x')
    assert aa.folder_has_cover_sidecar(str(tmp_path)) is True


def test_album_has_art_on_disk_no_local_file_is_true():
    # No representative file (e.g. media-server-only album) → not flagged.
    assert aa.album_has_art_on_disk('') is True
    assert aa.album_has_art_on_disk(None) is True


def test_album_has_art_on_disk_sidecar_short_circuits(tmp_path, monkeypatch):
    (tmp_path / 'cover.jpg').write_bytes(b'x')
    track = tmp_path / '01 song.flac'
    track.write_bytes(b'')
    # Sidecar present → True without ever opening the audio file.
    called = {'n': 0}
    monkeypatch.setattr(aa, 'file_has_embedded_art', lambda p: called.__setitem__('n', called['n'] + 1) or False)
    assert aa.album_has_art_on_disk(str(track)) is True
    assert called['n'] == 0


def test_album_has_art_on_disk_no_sidecar_checks_file(tmp_path, monkeypatch):
    track = tmp_path / '01 song.flac'
    track.write_bytes(b'')
    monkeypatch.setattr(aa, 'file_has_embedded_art', lambda p: False)
    assert aa.album_has_art_on_disk(str(track)) is False
    monkeypatch.setattr(aa, 'file_has_embedded_art', lambda p: True)
    assert aa.album_has_art_on_disk(str(track)) is True


# ── embedded-art detection ──

def _fake_symbols(audio):
    return SimpleNamespace(
        File=lambda path: audio,
        ID3=type('ID3', (), {}),
        MP4=type('MP4', (), {}),
        FLAC=type('FLAC', (), {}),
    )


def test_file_has_embedded_art_flac_picture(tmp_path, monkeypatch):
    f = tmp_path / 'a.flac'
    f.write_bytes(b'')
    audio = SimpleNamespace(pictures=['pic'], tags=None)
    monkeypatch.setattr(aa, 'get_mutagen_symbols', lambda: _fake_symbols(audio))
    assert aa.file_has_embedded_art(str(f)) is True


def test_file_has_embedded_art_none(tmp_path, monkeypatch):
    f = tmp_path / 'a.flac'
    f.write_bytes(b'')
    audio = SimpleNamespace(pictures=[], tags=None)
    monkeypatch.setattr(aa, 'get_mutagen_symbols', lambda: _fake_symbols(audio))
    assert aa.file_has_embedded_art(str(f)) is False


# ── applying art ──

def test_apply_embeds_into_each_file_and_writes_cover(tmp_path, monkeypatch):
    f1 = tmp_path / '01.flac'; f1.write_bytes(b'')
    f2 = tmp_path / '02.flac'; f2.write_bytes(b'')

    saved = []
    audio = SimpleNamespace(tags=None, add_tags=lambda: None, save=lambda: saved.append(True))
    monkeypatch.setattr(aa, 'get_mutagen_symbols', lambda: _fake_symbols(audio))

    embed_calls = []
    monkeypatch.setattr(aa, 'embed_album_art_metadata', lambda a, m: embed_calls.append(m) or True)
    # download_cover_art is the standard cover.jpg writer — stub it to drop one.
    monkeypatch.setattr(aa, 'download_cover_art',
                        lambda album_info, folder, ctx=None, **k: open(f"{folder}/cover.jpg", 'wb').close())

    meta = {'artist': 'A', 'album': 'B', 'album_art_url': 'http://x/y.jpg'}
    res = aa.apply_art_to_album_files([str(f1), str(f2)], meta, {'album_name': 'B'}, folder=str(tmp_path))

    assert res['embedded'] == 2
    assert len(saved) == 2          # each file saved after embed
    assert res['cover_written'] is True


def test_apply_skips_files_that_already_have_art(tmp_path, monkeypatch):
    """Purely additive: a file that already has art is left untouched (no
    duplicate FLAC picture, no re-embed)."""
    f1 = tmp_path / '01.flac'; f1.write_bytes(b'')
    audio = SimpleNamespace(pictures=['existing'], tags=None, save=lambda: None)
    monkeypatch.setattr(aa, 'get_mutagen_symbols', lambda: _fake_symbols(audio))
    embed_calls = []
    monkeypatch.setattr(aa, 'embed_album_art_metadata', lambda a, m: embed_calls.append(m) or True)
    monkeypatch.setattr(aa, 'download_cover_art', lambda *a, **k: None)

    res = aa.apply_art_to_album_files([str(f1)], {'album': 'B'}, {'album_name': 'B'}, folder=str(tmp_path))
    assert res['embedded'] == 0
    assert res['skipped'] == 1
    assert embed_calls == []      # never re-embedded → no duplicate picture


def test_apply_counts_failures_without_raising(tmp_path, monkeypatch):
    f1 = tmp_path / '01.flac'; f1.write_bytes(b'')
    audio = SimpleNamespace(tags=object(), save=lambda: (_ for _ in ()).throw(OSError('read-only')))
    monkeypatch.setattr(aa, 'get_mutagen_symbols', lambda: _fake_symbols(audio))
    monkeypatch.setattr(aa, 'embed_album_art_metadata', lambda a, m: True)
    monkeypatch.setattr(aa, 'download_cover_art', lambda *a, **k: None)

    res = aa.apply_art_to_album_files([str(f1)], {'album': 'B'}, {'album_name': 'B'}, folder=str(tmp_path))
    assert res['embedded'] == 0
    assert res['failed'] == 1       # save() raised (read-only) — counted, not crashed


# ── read-only filesystem detection (Tim: docker ':ro' mount, Errno 30) ──


def test_apply_does_not_trust_statvfs_writable_fs_succeeds(tmp_path, monkeypatch):
    """Regression (Sokhi): a WRITABLE union/FUSE/NFS mount can misreport
    ST_RDONLY in statvfs. The apply must NOT use statvfs — it writes anyway,
    and only the actual write decides. Here the embed succeeds even though
    statvfs (if it were consulted) would claim read-only."""
    f = tmp_path / 'a.mp3'
    f.write_bytes(b'')
    saved = []
    audio = SimpleNamespace(pictures=[], tags=None, add_tags=lambda: None,
                            save=lambda: saved.append(True))
    monkeypatch.setattr(aa, 'get_mutagen_symbols', lambda: _fake_symbols(audio))
    monkeypatch.setattr(aa, 'embed_album_art_metadata', lambda *a, **k: True)
    monkeypatch.setattr(aa, 'download_cover_art', lambda *a, **k: None)
    # Even if statvfs lies and says read-only, the write still happens.
    monkeypatch.setattr(aa.os, 'statvfs',
                        lambda p: SimpleNamespace(f_flag=getattr(aa.os, 'ST_RDONLY', 1)),
                        raising=False)

    res = aa.apply_art_to_album_files([str(f)], {}, {}, folder=str(tmp_path))

    assert res['embedded'] == 1 and saved == [True]
    assert res['read_only_fs'] is False      # statvfs ignored; write succeeded


def test_apply_flags_erofs_from_actual_write(tmp_path, monkeypatch):
    """read-only is detected from a real EROFS on write (the only honest test),
    and it fast-fails the remaining files."""
    import errno as _errno
    f1 = tmp_path / 'a.mp3'; f1.write_bytes(b'')
    f2 = tmp_path / 'b.mp3'; f2.write_bytes(b'')

    def _boom():
        raise OSError(_errno.EROFS, 'Read-only file system')
    audio = SimpleNamespace(pictures=[], tags={'ok': 1}, save=_boom)
    monkeypatch.setattr(aa, 'get_mutagen_symbols', lambda: _fake_symbols(audio))
    monkeypatch.setattr(aa, 'embed_album_art_metadata', lambda *a, **k: True)
    monkeypatch.setattr(aa, 'download_cover_art', lambda *a, **k: None)

    res = aa.apply_art_to_album_files([str(f1), str(f2)], {}, {}, folder=str(tmp_path))

    assert res['read_only_fs'] is True
    assert res['failed'] == 2                 # first EROFS fails it + bails the rest


def test_cover_only_read_only_is_detected(tmp_path, monkeypatch):
    """The gap that left Sokhi stuck (#804): tracks already have art so the
    embed loop is skipped, but the cover.jpg write hits a read-only mount.
    download_cover_art swallows that EROFS onto the context — apply must still
    surface read_only_fs (not report a silent success)."""
    f = tmp_path / 'a.flac'; f.write_bytes(b'')
    audio = SimpleNamespace(pictures=['pic'], tags={'ok': 1})  # already has art → embed skipped
    monkeypatch.setattr(aa, 'get_mutagen_symbols', lambda: _fake_symbols(audio))

    def _fake_download(album_info, target_dir, ctx, **k):
        # mimic download_cover_art's EROFS handling: record on context, swallow
        if isinstance(ctx, dict):
            ctx['_cover_read_only'] = True
    monkeypatch.setattr(aa, 'download_cover_art', _fake_download)

    res = aa.apply_art_to_album_files([str(f)], {}, {}, folder=str(tmp_path))

    assert res['embedded'] == 0 and res['skipped'] == 1   # nothing embedded
    assert res['read_only_fs'] is True                    # but read-only surfaced


def test_apply_normal_failure_not_flagged_read_only(tmp_path, monkeypatch):
    def _boom():
        raise PermissionError(13, 'Permission denied')
    f = tmp_path / 'a.mp3'
    f.write_bytes(b'')
    audio = SimpleNamespace(pictures=[], tags={'ok': 1}, save=_boom)
    monkeypatch.setattr(aa, 'get_mutagen_symbols', lambda: _fake_symbols(audio))
    monkeypatch.setattr(aa, 'embed_album_art_metadata', lambda *a, **k: True)
    monkeypatch.setattr(aa, 'download_cover_art', lambda *a, **k: None)

    res = aa.apply_art_to_album_files([str(f)], {}, {}, folder=str(tmp_path))

    assert res['read_only_fs'] is False      # EACCES is chmod-able, EROFS is not
    assert res['failed'] == 1




def test_filler_forces_cover_sidecar_write(tmp_path, monkeypatch):
    # The Cover Art Filler must write cover.jpg regardless of the import-time
    # "Download cover.jpg" toggle — so apply passes force=True (Sokhi).
    f = tmp_path / 'a.mp3'; f.write_bytes(b'')
    audio = SimpleNamespace(pictures=[], tags={'ok': 1}, save=lambda: None)
    monkeypatch.setattr(aa, 'get_mutagen_symbols', lambda: _fake_symbols(audio))
    monkeypatch.setattr(aa, 'embed_album_art_metadata', lambda *a, **k: True)
    captured = {}
    monkeypatch.setattr(aa, 'download_cover_art',
                        lambda album_info, target_dir, ctx, **k: captured.update(k))

    aa.apply_art_to_album_files([str(f)], {}, {}, folder=str(tmp_path))

    assert captured.get('force') is True


# ── cover.jpg from embedded art (#813/Sokhi: files have art, no sidecar) ──

def test_extract_embedded_art_flac(tmp_path, monkeypatch):
    f = tmp_path / 'a.flac'; f.write_bytes(b'')
    audio = SimpleNamespace(pictures=[SimpleNamespace(data=b'IMGBYTES')], tags=None)
    monkeypatch.setattr(aa, 'get_mutagen_symbols', lambda: _fake_symbols(audio))
    assert aa.extract_embedded_art(str(f)) == b'IMGBYTES'


def test_extract_embedded_art_none_when_no_pictures(tmp_path, monkeypatch):
    f = tmp_path / 'a.flac'; f.write_bytes(b'')
    audio = SimpleNamespace(pictures=[], tags=None)
    monkeypatch.setattr(aa, 'get_mutagen_symbols', lambda: _fake_symbols(audio))
    assert aa.extract_embedded_art(str(f)) is None


def test_apply_writes_cover_jpg_from_embedded_art(tmp_path, monkeypatch):
    # Files already have embedded art, no cover.jpg → apply extracts it and
    # writes the sidecar WITHOUT an API fetch (consistent + offline).
    f = tmp_path / '01.flac'; f.write_bytes(b'')
    audio = SimpleNamespace(pictures=[SimpleNamespace(data=b'EMBEDDED')], tags=None, save=lambda: None)
    monkeypatch.setattr(aa, 'get_mutagen_symbols', lambda: _fake_symbols(audio))
    monkeypatch.setattr(aa, 'embed_album_art_metadata', lambda *a, **k: True)
    dl_called = []
    monkeypatch.setattr(aa, 'download_cover_art', lambda *a, **k: dl_called.append(1))

    res = aa.apply_art_to_album_files([str(f)], {}, {}, folder=str(tmp_path))

    assert (tmp_path / 'cover.jpg').read_bytes() == b'EMBEDDED'
    assert res['cover_written'] is True
    assert dl_called == []          # used embedded art — no API call


def test_apply_falls_back_to_download_when_no_embedded(tmp_path, monkeypatch):
    # No embedded art to extract → fetch the sidecar via download_cover_art.
    f = tmp_path / '01.flac'; f.write_bytes(b'')
    audio = SimpleNamespace(pictures=[], tags=None, add_tags=lambda: None, save=lambda: None)
    monkeypatch.setattr(aa, 'get_mutagen_symbols', lambda: _fake_symbols(audio))
    monkeypatch.setattr(aa, 'embed_album_art_metadata', lambda *a, **k: True)
    dl_called = []

    def fake_dl(album_info, target, ctx=None, **k):
        dl_called.append(1)
        open(f"{target}/cover.jpg", 'wb').close()
    monkeypatch.setattr(aa, 'download_cover_art', fake_dl)

    res = aa.apply_art_to_album_files([str(f)], {'album_art_url': 'http://x/y.jpg'},
                                      {'album_name': 'B'}, folder=str(tmp_path))
    assert dl_called == [1]          # no embedded art → API fetch
    assert res['cover_written'] is True


def test_apply_skips_cover_when_sidecar_exists(tmp_path, monkeypatch):
    # cover.jpg already present → don't extract or download.
    (tmp_path / 'cover.jpg').write_bytes(b'EXISTING')
    f = tmp_path / '01.flac'; f.write_bytes(b'')
    audio = SimpleNamespace(pictures=[SimpleNamespace(data=b'X')], tags=None, save=lambda: None)
    monkeypatch.setattr(aa, 'get_mutagen_symbols', lambda: _fake_symbols(audio))
    monkeypatch.setattr(aa, 'embed_album_art_metadata', lambda *a, **k: True)
    dl_called = []
    monkeypatch.setattr(aa, 'download_cover_art', lambda *a, **k: dl_called.append(1))

    aa.apply_art_to_album_files([str(f)], {}, {}, folder=str(tmp_path))
    assert (tmp_path / 'cover.jpg').read_bytes() == b'EXISTING'   # untouched
    assert dl_called == []


def test_apply_cover_falls_back_to_file_dir_when_folder_doesnt_exist(tmp_path, monkeypatch):
    # The Sokhi bug: the caller passed the raw DB folder (e.g. Jellyfin's
    # /data/music/...) which doesn't exist in the container, so the cover.jpg
    # write was silently skipped. Now it falls back to the real directory of
    # the files and writes there anyway.
    real_dir = tmp_path / 'real'
    real_dir.mkdir()
    f = real_dir / '01.flac'; f.write_bytes(b'')
    audio = SimpleNamespace(pictures=[SimpleNamespace(data=b'EMB')], tags=None, save=lambda: None)
    monkeypatch.setattr(aa, 'get_mutagen_symbols', lambda: _fake_symbols(audio))
    monkeypatch.setattr(aa, 'embed_album_art_metadata', lambda *a, **k: True)
    monkeypatch.setattr(aa, 'download_cover_art', lambda *a, **k: None)

    res = aa.apply_art_to_album_files([str(f)], {}, {},
                                      folder='/data/music/does/not/exist/in/container')

    assert (real_dir / 'cover.jpg').read_bytes() == b'EMB'   # written to the REAL dir
    assert res['cover_written'] is True
