import os.path
from shutil import rmtree
from tempfile import mkdtemp
from unittest.mock import Mock, call, patch

import pytest

from beets.test.helper import BeetsTestCase
from beets.util import bytestring_path, syspath
from beetsplug.thumbnails import (
    LARGE_DIR,
    NORMAL_DIR,
    GioURI,
    PathlibURI,
    ThumbnailsPlugin,
)


class ThumbnailsTest(BeetsTestCase):
    @patch("beetsplug.thumbnails.ArtResizer")
    @patch("beetsplug.thumbnails.ThumbnailsPlugin._check_local_ok", Mock())
    @patch("beetsplug.thumbnails.os.stat")
    def test_add_tags(self, mock_stat, mock_artresizer):
        plugin = ThumbnailsPlugin()
        plugin.get_uri = Mock(
            side_effect={b"/path/to/cover": "COVER_URI"}.__getitem__
        )
        album = Mock(artpath=b"/path/to/cover")
        mock_stat.return_value.st_mtime = 12345

        plugin.add_tags(album, b"/path/to/thumbnail")

        metadata = {"Thumb::URI": "COVER_URI", "Thumb::MTime": "12345"}
        mock_artresizer.shared.write_metadata.assert_called_once_with(
            b"/path/to/thumbnail", metadata
        )
        mock_stat.assert_called_once_with(syspath(album.artpath))

    @patch("beetsplug.thumbnails.os")
    @patch("beetsplug.thumbnails.ArtResizer")
    @patch("beetsplug.thumbnails.GioURI")
    def test_check_local_ok(self, mock_giouri, mock_artresizer, mock_os):
        # test local resizing capability
        mock_artresizer.shared.local = False
        mock_artresizer.shared.can_write_metadata = False
        plugin = ThumbnailsPlugin()
        assert not plugin._check_local_ok()

        # test dirs creation
        mock_artresizer.shared.local = True
        mock_artresizer.shared.can_write_metadata = True

        def exists(path):
            if path == syspath(NORMAL_DIR):
                return False
            if path == syspath(LARGE_DIR):
                return True
            raise ValueError(f"unexpected path {path!r}")

        mock_os.path.exists = exists
        plugin = ThumbnailsPlugin()
        mock_os.makedirs.assert_called_once_with(syspath(NORMAL_DIR))
        assert plugin._check_local_ok()

        # test metadata writer function
        mock_os.path.exists = lambda _: True

        mock_artresizer.shared.local = True
        mock_artresizer.shared.can_write_metadata = False
        with pytest.raises(RuntimeError):
            ThumbnailsPlugin()

        mock_artresizer.shared.local = True
        mock_artresizer.shared.can_write_metadata = True
        assert ThumbnailsPlugin()._check_local_ok()

        # test URI getter function
        giouri_inst = mock_giouri.return_value
        giouri_inst.available = True
        assert ThumbnailsPlugin().get_uri == giouri_inst.uri

        giouri_inst.available = False
        assert ThumbnailsPlugin().get_uri.__self__.__class__ == PathlibURI

    @patch("beetsplug.thumbnails.ThumbnailsPlugin._check_local_ok", Mock())
    @patch("beetsplug.thumbnails.ArtResizer")
    @patch("beets.util.syspath", Mock(side_effect=lambda x: x))
    @patch("beetsplug.thumbnails.os")
    @patch("beetsplug.thumbnails.shutil")
    def test_make_cover_thumbnail(self, mock_shutils, mock_os, mock_artresizer):
        thumbnail_dir = os.path.normpath(b"/thumbnail/dir")
        md5_file = os.path.join(thumbnail_dir, b"md5")
        path_to_art = os.path.normpath(b"/path/to/art")
        path_to_resized_art = os.path.normpath(b"/path/to/resized/artwork")

        mock_os.path.join = os.path.join  # don't mock that function
        plugin = ThumbnailsPlugin()
        plugin.add_tags = Mock()

        album = Mock(artpath=path_to_art)
        plugin.thumbnail_file_name = Mock(return_value=b"md5")
        mock_os.path.exists.return_value = False

        mock_resize = mock_artresizer.shared.resize
        mock_resize.return_value = path_to_resized_art

        plugin.make_cover_thumbnail(album, 12345, thumbnail_dir)

        mock_os.path.exists.assert_called_once_with(syspath(md5_file))

        mock_resize.assert_called_once_with(12345, path_to_art, md5_file)
        plugin.add_tags.assert_called_once_with(album, path_to_resized_art)
        mock_shutils.move.assert_called_once_with(
            syspath(path_to_resized_art), syspath(md5_file)
        )

        # now test with recent thumbnail & with force
        mock_os.path.exists.return_value = True
        plugin.force = False
        mock_resize.reset_mock()

        def os_stat(target):
            if target == syspath(md5_file):
                return Mock(st_mtime=3)
            if target == syspath(path_to_art):
                return Mock(st_mtime=2)
            raise ValueError(f"invalid target {target}")

        mock_os.stat.side_effect = os_stat

        plugin.make_cover_thumbnail(album, 12345, thumbnail_dir)
        assert mock_resize.call_count == 0

        # and with force
        plugin.config["force"] = True
        plugin.make_cover_thumbnail(album, 12345, thumbnail_dir)
        mock_resize.assert_called_once_with(12345, path_to_art, md5_file)

    @patch("beetsplug.thumbnails.ThumbnailsPlugin._check_local_ok", Mock())
    def test_make_dolphin_cover_thumbnail(self):
        plugin = ThumbnailsPlugin()
        tmp = bytestring_path(mkdtemp())
        album = Mock(path=tmp, artpath=os.path.join(tmp, b"cover.jpg"))
        plugin.make_dolphin_cover_thumbnail(album)
        with open(os.path.join(tmp, b".directory"), "rb") as f:
            assert f.read().splitlines() == [
                b"[Desktop Entry]",
                b"Icon=./cover.jpg",
            ]

        # not rewritten when it already exists (yup that's a big limitation)
        album.artpath = b"/my/awesome/art.tiff"
        plugin.make_dolphin_cover_thumbnail(album)
        with open(os.path.join(tmp, b".directory"), "rb") as f:
            assert f.read().splitlines() == [
                b"[Desktop Entry]",
                b"Icon=./cover.jpg",
            ]

        rmtree(syspath(tmp))

    @patch("beetsplug.thumbnails.ThumbnailsPlugin._check_local_ok", Mock())
    @patch("beetsplug.thumbnails.ArtResizer")
    def test_process_album(self, mock_artresizer):
        get_size = mock_artresizer.shared.get_size

        plugin = ThumbnailsPlugin()
        make_cover = plugin.make_cover_thumbnail = Mock(return_value=True)
        make_dolphin = plugin.make_dolphin_cover_thumbnail = Mock()

        # no art
        album = Mock(artpath=None)
        plugin.process_album(album)
        assert get_size.call_count == 0
        assert make_dolphin.call_count == 0

        # cannot get art size
        album.artpath = b"/path/to/art"
        get_size.return_value = None
        plugin.process_album(album)
        get_size.assert_called_once_with(b"/path/to/art")
        assert make_cover.call_count == 0

        # dolphin tests
        plugin.config["dolphin"] = False
        plugin.process_album(album)
        assert make_dolphin.call_count == 0

        plugin.config["dolphin"] = True
        plugin.process_album(album)
        make_dolphin.assert_called_once_with(album)

        # small art
        get_size.return_value = 200, 200
        plugin.process_album(album)
        make_cover.assert_called_once_with(album, 128, NORMAL_DIR)

        # big art
        make_cover.reset_mock()
        get_size.return_value = 500, 500
        plugin.process_album(album)
        make_cover.assert_has_calls(
            [call(album, 128, NORMAL_DIR), call(album, 256, LARGE_DIR)],
            any_order=True,
        )

    @patch("beetsplug.thumbnails.ThumbnailsPlugin._check_local_ok", Mock())
    def test_invokations(self):
        plugin = ThumbnailsPlugin()
        plugin.process_album = Mock()
        album = Mock()

        plugin.process_album.reset_mock()
        lib = Mock()
        album2 = Mock()
        lib.albums.return_value = [album, album2]
        plugin.process_query(lib, Mock(), None)
        plugin.process_album.assert_has_calls(
            [call(album), call(album2)], any_order=True
        )

    @patch("beetsplug.thumbnails.BaseDirectory")
    def test_thumbnail_file_name(self, mock_basedir):
        plug = ThumbnailsPlugin()
        plug.get_uri = Mock(return_value="file:///my/uri")
        assert (
            plug.thumbnail_file_name(b"idontcare")
            == b"9488f5797fbe12ffb316d607dfd93d04.png"
        )

    def test_uri(self):
        gio = GioURI()
        if not gio.available:
            self.skipTest("GIO library not found")

        import ctypes

        with pytest.raises(ctypes.ArgumentError):
            gio.uri("/foo")
        assert gio.uri(b"/foo") == "file:///foo"
        assert gio.uri(b"/foo!") == "file:///foo!"
        assert (
            gio.uri(b"/music/\xec\x8b\xb8\xec\x9d\xb4")
            == "file:///music/%EC%8B%B8%EC%9D%B4"
        )


class TestPathlibURI:
    """Test PathlibURI class"""

    def test_uri(self):
        test_uri = PathlibURI()

        # test it won't break if we pass it bytes for a path
        test_uri.uri(b"/")
