# -*- coding: utf-8 -*-
#
# Picard, the next-generation MusicBrainz tagger
#
# Copyright (C) 2019 Zenara Daley
# Copyright (C) 2019-2021 Philipp Wolfer
# Copyright (C) 2020-2022 Laurent Monin
# Copyright (C) 2022 Marcin Szalowicz
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.


import os.path
import unittest

import mutagen

from test.picardtestcase import PicardTestCase

from picard import config
from picard.file import File
import picard.formats
from picard.formats import ext_to_format
from picard.formats.mutagenext.aac import AACAPEv2
from picard.formats.mutagenext.ac3 import AC3APEv2
from picard.formats.mutagenext.tak import TAK
from picard.formats.util import guess_format
from picard.metadata import Metadata


settings = {
    'clear_existing_tags': False,
    'preserve_images': False,
    'embed_only_one_front_image': False,
    'enabled_plugins': '',
    'id3v23_join_with': '/',
    'id3v2_encoding': 'utf-8',
    'rating_steps': 6,
    'rating_user_email': 'users@musicbrainz.org',
    'remove_ape_from_mp3': False,
    'remove_id3_from_flac': False,
    'fix_missing_seekpoints_flac': False,
    'remove_images_from_tags': False,
    'save_images_to_tags': True,
    'write_id3v1': True,
    'write_id3v23': False,
    'itunes_compatible_grouping': False,
    'aac_save_ape': True,
    'ac3_save_ape': True,
    'write_wave_riff_info': True,
    'remove_wave_riff_info': False,
    'wave_riff_info_encoding': 'iso-8859-1',
    'replace_spaces_with_underscores': False,
    'replace_dir_separator': '_',
    'win_compat_replacements': {},
}


def save_metadata(filename, metadata):
    f = picard.formats.open_(filename)
    loaded_metadata = f._load(filename)
    f._copy_loaded_metadata(loaded_metadata)
    f._save(filename, metadata)


def load_metadata(filename):
    f = picard.formats.open_(filename)
    return f._load(filename)


def save_and_load_metadata(filename, metadata):
    """Save new metadata to a file and load it again."""
    save_metadata(filename, metadata)
    return load_metadata(filename)


def load_raw(filename):
    # First try special implementations in Picard
    f = mutagen.File(filename, [AACAPEv2, AC3APEv2, TAK])
    if f is None:
        f = mutagen.File(filename)
    return f


def save_raw(filename, tags):
    f = load_raw(filename)
    for k, v in tags.items():
        f[k] = v
    f.save()


TAGS = {
    'albumartist': 'Foo',
    'albumartistsort': 'Foo',
    'album': 'Foo Bar',
    'albumsort': 'Foo',
    'arranger': 'Foo',
    'artist': 'Foo',
    'artistsort': 'Foo',
    'asin': 'Foo',
    'barcode': 'Foo',
    'bpm': '80',
    'catalognumber': 'Foo',
    'comment': 'Foo',
    'comment:foo': 'Foo',
    'comment:deu:foo': 'Foo',
    'compilation': '1',
    'composer': 'Foo',
    'composersort': 'Foo',
    'conductor': 'Foo',
    'copyright': 'Foo',
    'date': '2004',
    'director': 'Foo',
    'discnumber': '1',
    'discsubtitle': 'Foo',
    'djmixer': 'Foo',
    'encodedby': 'Foo',
    'encodersettings': 'Foo',
    'engineer': 'Foo',
    'gapless': '1',
    'genre': 'Foo',
    'grouping': 'Foo',
    'isrc': 'Foo',
    'key': 'E#m',
    'label': 'Foo',
    'lyricist': 'Foo',
    'lyrics': 'Foo',
    'media': 'Foo',
    'mixer': 'Foo',
    'mood': 'Foo',
    'movement': 'Foo',
    'movementnumber': '2',
    'movementtotal': '8',
    'musicbrainz_albumartistid': '00000000-0000-0000-0000-000000000000',
    'musicbrainz_albumid': '00000000-0000-0000-0000-000000000000',
    'musicbrainz_artistid': '00000000-0000-0000-0000-000000000000',
    'musicbrainz_discid': 'HJRFvVfxx0MU_6v8v9swQUxDmZQ-',
    'musicbrainz_originalalbumid': '00000000-0000-0000-0000-000000000000',
    'musicbrainz_originalartistid': '00000000-0000-0000-0000-000000000000',
    'musicbrainz_releasegroupid': '00000000-0000-0000-0000-000000000000',
    'musicbrainz_trackid': '00000000-0000-0000-0000-000000000000',
    'musicbrainz_trmid': 'Foo',
    'musicbrainz_workid': '00000000-0000-0000-0000-000000000000',
    'musicip_fingerprint': 'Foo',
    'musicip_puid': '00000000-0000-0000-0000-000000000000',
    'originaldate': '1980-01-20',
    'originalyear': '1980',
    'originalfilename': 'Foo',
    'performer': 'Foo',
    'performer:guest vocal': 'Foo',
    'podcast': '1',
    'podcasturl': 'Foo',
    'producer': 'Foo',
    'releasedate': '2022-05-22',
    'releasecountry': 'XW',
    'releasestatus': 'Foo',
    'releasetype': 'Foo',
    'remixer': 'Foo',
    'show': 'Foo',
    'showmovement': '1',
    'showsort': 'Foo',
    'subtitle': 'Foo',
    'title': 'Foo',
    'titlesort': 'Foo',
    'totaldiscs': '2',
    'totaltracks': '10',
    'tracknumber': '2',
    'website': 'http://example.com',
    'work': 'Foo'
}

REPLAYGAIN_TAGS = {
    'replaygain_album_gain': '-6.48 dB',
    'replaygain_album_peak': '0.978475',
    'replaygain_album_range': '7.84 dB',
    'replaygain_track_gain': '-6.16 dB',
    'replaygain_track_peak': '0.976991',
    'replaygain_track_range': '8.22 dB',
    'replaygain_reference_loudness': '-18.00 LUFS',
}


def skipUnlessTestfile(func):
    def _decorator(self, *args, **kwargs):
        if not self.testfile:
            raise unittest.SkipTest("No test file set")
        func(self, *args, **kwargs)
    return _decorator


# prevent unittest to run tests in those classes
class CommonTests:

    class BaseFileTestCase(PicardTestCase):
        testfile = None
        testfile_ext = None
        testfile_path = None

        def setUp(self):
            super().setUp()
            self.set_config_values(settings)
            if self.testfile:
                _name, self.testfile_ext = os.path.splitext(self.testfile)
                self.testfile_path = os.path.join('test', 'data', self.testfile)
                self.testfile_ext = os.path.splitext(self.testfile)[1]
                self.filename = self.copy_of_original_testfile()
                self.format = ext_to_format(self.testfile_ext[1:])

        def copy_of_original_testfile(self):
            return self.copy_file_tmp(self.testfile_path, self.testfile_ext)

    class SimpleFormatsTestCase(BaseFileTestCase):

        expected_info = {}
        unexpected_info = []

        @skipUnlessTestfile
        def test_can_open_and_save(self):
            metadata = save_and_load_metadata(self.filename, Metadata())
            self.assertTrue(metadata['~format'])

        @skipUnlessTestfile
        def test_info(self):
            if not self.expected_info:
                raise unittest.SkipTest("Ratings not supported for %s" % self.format.NAME)
            metadata = save_and_load_metadata(self.filename, Metadata())
            for key, expected_value in self.expected_info.items():
                value = metadata.length if key == 'length' else metadata[key]
                self.assertEqual(expected_value, value, '%s: %r != %r' % (key, expected_value, value))
            for key in self.unexpected_info:
                self.assertNotIn(key, metadata)

        def _test_supported_tags(self, tags):
            metadata = Metadata(tags)
            loaded_metadata = save_and_load_metadata(self.filename, metadata)
            for (key, value) in tags.items():
                self.assertEqual(loaded_metadata[key], value, '%s: %r != %r' % (key, loaded_metadata[key], value))

        def _test_unsupported_tags(self, tags):
            metadata = Metadata(tags)
            loaded_metadata = save_and_load_metadata(self.filename, metadata)
            for tag in tags:
                self.assertFalse(self.format.supports_tag(tag))
                self.assertNotIn(tag, loaded_metadata, '%s: %r != None' % (tag, loaded_metadata[tag]))

    class TagFormatsTestCase(SimpleFormatsTestCase):

        def setUp(self):
            super().setUp()
            self.tags = TAGS.copy()
            self.replaygain_tags = REPLAYGAIN_TAGS.copy()
            self.unsupported_tags = {}
            self.setup_tags()

        def setup_tags(self):
            if self.testfile:
                supports_tag = self.format.supports_tag
                self.unsupported_tags = {tag: val for tag, val in self.tags.items() if not supports_tag(tag)}
                self.remove_tags(self.unsupported_tags.keys())

        def set_tags(self, dict_tag_value=None):
            if dict_tag_value:
                self.tags.update(dict_tag_value)

        def remove_tags(self, tag_list=None):
            for tag in tag_list:
                del self.tags[tag]

        @skipUnlessTestfile
        def test_simple_tags(self):
            self._test_supported_tags(self.tags)

        @skipUnlessTestfile
        def test_replaygain_tags(self):
            self._test_supported_tags(self.replaygain_tags)

        @skipUnlessTestfile
        def test_replaygain_tags_case_insensitive(self):
            tags = {
                'replaygain_album_gain': '-6.48 dB',
                'Replaygain_Album_Peak': '0.978475',
                'replaygain_album_range': '7.84 dB',
                'replaygain_track_gain': '-6.16 dB',
                'replaygain_track_peak': '0.976991',
                'replaygain_track_range': '8.22 dB',
                'replaygain_reference_loudness': '-18.00 LUFS',
            }
            save_raw(self.filename, tags)
            loaded_metadata = load_metadata(self.filename)
            for (key, value) in self.replaygain_tags.items():
                self.assertEqual(loaded_metadata[key], value, '%s: %r != %r' % (key, loaded_metadata[key], value))

        @skipUnlessTestfile
        def test_save_does_not_modify_metadata(self):
            tags = dict(self.tags)
            if self.supports_ratings:
                tags['~rating'] = '3'
            metadata = Metadata(tags)
            save_metadata(self.filename, metadata)
            for (key, value) in tags.items():
                self.assertEqual(metadata[key], value, '%s: %r != %r' % (key, metadata[key], value))

        @skipUnlessTestfile
        def test_unsupported_tags(self):
            self._test_unsupported_tags(self.unsupported_tags)

        @skipUnlessTestfile
        def test_unsupported_tags_info_tags(self):
            for tag in File.FILE_INFO_TAGS:
                self.assertFalse(self.format.supports_tag(tag), 'Tag "%s" must not be supported' % tag)

        @skipUnlessTestfile
        def test_preserve_unchanged_tags(self):
            metadata = Metadata(self.tags)
            save_metadata(self.filename, metadata)
            loaded_metadata = save_and_load_metadata(self.filename, Metadata())
            for (key, value) in self.tags.items():
                self.assertEqual(loaded_metadata[key], value, '%s: %r != %r' % (key, loaded_metadata[key], value))

        @skipUnlessTestfile
        def test_delete_simple_tags(self):
            metadata = Metadata(self.tags)
            if self.supports_ratings:
                metadata['~rating'] = 1
            original_metadata = save_and_load_metadata(self.filename, metadata)
            del metadata['albumartist']
            del metadata['musicbrainz_artistid']
            if self.supports_ratings:
                del metadata['~rating']
            new_metadata = save_and_load_metadata(self.filename, metadata)
            self.assertIn('albumartist', original_metadata)
            self.assertNotIn('albumartist', new_metadata)
            self.assertIn('musicbrainz_artistid', original_metadata)
            self.assertNotIn('musicbrainz_artistid', new_metadata)
            if self.supports_ratings:
                self.assertIn('~rating', original_metadata)
                self.assertNotIn('~rating', new_metadata)

        @skipUnlessTestfile
        def test_delete_tags_with_empty_description(self):
            for key in ('lyrics', 'lyrics:', 'comment', 'comment:', 'performer', 'performer:'):
                name = key.rstrip(':')
                name_with_description = name + ':foo'
                if not self.format.supports_tag(name):
                    continue
                metadata = Metadata()
                metadata[name] = 'bar'
                metadata[name_with_description] = 'other'
                original_metadata = save_and_load_metadata(self.filename, metadata)
                self.assertIn(name, original_metadata)
                del metadata[key]
                new_metadata = save_and_load_metadata(self.filename, metadata)
                self.assertNotIn(name, new_metadata)
                # Ensure the names with description did not get deleted
                if name_with_description in original_metadata:
                    self.assertIn(name_with_description, new_metadata)

        @skipUnlessTestfile
        def test_delete_tags_with_description(self):
            for key in (
                'comment:foo', 'comment:de:foo', 'performer:foo', 'lyrics:foo',
                'comment:a*', 'comment:a[', 'performer:(x)', 'performer: Ä é '
            ):
                if not self.format.supports_tag(key):
                    continue
                prefix = key.split(':')[0]
                metadata = Metadata()
                metadata[key] = 'bar'
                original_metadata = save_and_load_metadata(self.filename, metadata)
                if key not in original_metadata and prefix in original_metadata:
                    continue  # Skip if the type did not support saving this kind of tag
                self.assertEqual('bar', original_metadata[key], original_metadata)
                metadata[prefix] = '(foo) bar'
                del metadata[key]
                new_metadata = save_and_load_metadata(self.filename, metadata)
                self.assertNotIn(key, new_metadata)
                self.assertEqual('(foo) bar', new_metadata[prefix])

        @skipUnlessTestfile
        def test_delete_nonexistant_tags(self):
            for key in ('title', 'foo', 'comment:foo', 'comment:de:foo',
                        'performer:foo', 'lyrics:foo', 'totaltracks'):
                if not self.format.supports_tag(key):
                    continue
                metadata = Metadata()
                save_metadata(self.filename, metadata)
                del metadata[key]
                new_metadata = save_and_load_metadata(self.filename, metadata)
                self.assertNotIn(key, new_metadata)

        @skipUnlessTestfile
        def test_delete_complex_tags(self):
            metadata = Metadata(self.tags)
            original_metadata = save_and_load_metadata(self.filename, metadata)
            del metadata['totaldiscs']
            new_metadata = save_and_load_metadata(self.filename, metadata)

            self.assertIn('totaldiscs', original_metadata)
            if self.testfile_ext in {'.m4a', '.m4v'}:
                self.assertEqual('0', new_metadata['totaldiscs'])
            else:
                self.assertNotIn('totaldiscs', new_metadata)

        @skipUnlessTestfile
        def test_delete_performer(self):
            if not self.format.supports_tag('performer:'):
                raise unittest.SkipTest('Tag "performer:" not supported for %s' % self.format.NAME)
            metadata = Metadata({
                'performer:piano': ['Piano1', 'Piano2'],
                'performer:guitar': ['Guitar1'],
            })
            original_metadata = save_and_load_metadata(self.filename, metadata)
            self.assertIn('Piano1', original_metadata.getall('performer:piano'))
            self.assertIn('Piano2', original_metadata.getall('performer:piano'))
            self.assertEqual(2, len(original_metadata.getall('performer:piano')))
            self.assertEqual('Guitar1', original_metadata['performer:guitar'])

            del metadata['performer:piano']
            new_metadata = save_and_load_metadata(self.filename, metadata)
            self.assertNotIn('performer:piano', new_metadata)
            self.assertEqual('Guitar1', metadata['performer:guitar'])

        @skipUnlessTestfile
        def test_save_performer(self):
            if not self.format.supports_tag('performer:'):
                raise unittest.SkipTest('Tag "performer:" not supported for %s' % self.format.NAME)
            instrument = "accordéon clavier « boutons »"
            artist = "桑山哲也"
            tag = "performer:" + instrument
            metadata = Metadata({tag: artist})
            loaded_metadata = save_and_load_metadata(self.filename, metadata)
            self.assertIn(tag, loaded_metadata)
            self.assertEqual(artist, loaded_metadata[tag])

        @skipUnlessTestfile
        def test_ratings(self):
            if not self.supports_ratings:
                raise unittest.SkipTest("Ratings not supported")
            for rating in range(6):
                rating = 1
                metadata = Metadata()
                metadata['~rating'] = rating
                loaded_metadata = save_and_load_metadata(self.filename, metadata)
                self.assertEqual(int(loaded_metadata['~rating']), rating, '~rating: %r != %r' % (loaded_metadata['~rating'], rating))

        @skipUnlessTestfile
        def test_invalid_rating_email(self):
            if not self.supports_ratings:
                raise unittest.SkipTest("Ratings not supported")
            metadata = Metadata()
            metadata['~rating'] = 3
            config.setting['rating_user_email'] = '{in\tvälid}'
            loaded_metadata = save_and_load_metadata(self.filename, metadata)
            self.assertEqual(loaded_metadata['~rating'], metadata['~rating'])

        @skipUnlessTestfile
        def test_guess_format(self):
            temp_file = self.copy_of_original_testfile()
            audio = guess_format(temp_file)
            audio_original = picard.formats.open_(self.filename)
            self.assertEqual(type(audio), type(audio_original))

        @skipUnlessTestfile
        def test_split_ext(self):
            f = picard.formats.open_(self.filename)
            self.assertEqual(f._fixed_splitext(f.filename), os.path.splitext(f.filename))
            self.assertEqual(f._fixed_splitext('.test'), os.path.splitext('.test'))
            if f.EXTENSIONS:
                self.assertEqual(f._fixed_splitext(f.EXTENSIONS[0]), ('', f.EXTENSIONS[0]))
                self.assertNotEqual(f._fixed_splitext(f.EXTENSIONS[0]), os.path.splitext(f.EXTENSIONS[0]))

        @skipUnlessTestfile
        def test_clear_existing_tags_off(self):
            config.setting['clear_existing_tags'] = False
            existing_metadata = Metadata({'artist': 'The Artist'})
            save_metadata(self.filename, existing_metadata)
            new_metadata = Metadata({'title': 'The Title'})
            loaded_metadata = save_and_load_metadata(self.filename, new_metadata)
            self.assertEqual(existing_metadata['artist'], loaded_metadata['artist'])
            self.assertEqual(new_metadata['title'], loaded_metadata['title'])

        @skipUnlessTestfile
        def test_clear_existing_tags_on(self):
            config.setting['clear_existing_tags'] = True
            existing_metadata = Metadata({'artist': 'The Artist'})
            save_metadata(self.filename, existing_metadata)
            new_metadata = Metadata({'title': 'The Title'})
            loaded_metadata = save_and_load_metadata(self.filename, new_metadata)
            self.assertNotIn('artist', loaded_metadata)
            self.assertEqual(new_metadata['title'], loaded_metadata['title'])

        @skipUnlessTestfile
        def test_lyrics_with_description(self):
            metadata = Metadata({'lyrics:foó': 'bar'})
            loaded_metadata = save_and_load_metadata(self.filename, metadata)
            self.assertEqual(metadata['lyrics:foó'], loaded_metadata['lyrics'])

        @skipUnlessTestfile
        def test_comments_with_description(self):
            if not self.format.supports_tag('comment:foó'):
                return
            metadata = Metadata({'comment:foó': 'bar'})
            loaded_metadata = save_and_load_metadata(self.filename, metadata)
            self.assertEqual(metadata['comment:foó'], loaded_metadata['comment:foó'])

        @skipUnlessTestfile
        def test_invalid_track_and_discnumber(self):
            # This test assumes a non-numeric test number can be written. For
            # formats not supporting this it needs to be overridden.
            metadata = Metadata({
                'discnumber': 'notanumber',
                'tracknumber': 'notanumber',
            })
            loaded_metadata = save_and_load_metadata(self.filename, metadata)
            self.assertEqual(loaded_metadata['discnumber'], metadata['discnumber'])
            self.assertEqual(loaded_metadata['totaldiscs'], metadata['totaldiscs'])
            self.assertEqual(loaded_metadata['tracknumber'], metadata['tracknumber'])
            self.assertEqual(loaded_metadata['totaltracks'], metadata['totaltracks'])

        @skipUnlessTestfile
        def test_save_movementnumber_without_movementtotal(self):
            if not self.format.supports_tag('movementnumber'):
                raise unittest.SkipTest('Tag "movementnumber" not supported for %s' % self.format.NAME)
            metadata = Metadata({'movementnumber': 7})
            loaded_metadata = save_and_load_metadata(self.filename, metadata)
            self.assertEqual(loaded_metadata['movementnumber'], metadata['movementnumber'])
            self.assertNotIn('movementtotal', loaded_metadata)
