# -*- coding: utf-8 -*-
# This file is part of MediaFile.
# Copyright 2016, Adrian Sampson.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.

"""Automatically-generated blanket testing for the MediaFile metadata
layer.
"""
from __future__ import division, absolute_import, print_function

import os
import shutil
import datetime
import time
import unittest
from six import assertCountEqual

from test import _common
from mediafile import MediaFile, Image, \
    ImageType, CoverArtField, UnreadableFileError
import mutagen


class ArtTestMixin(object):
    """Test reads and writes of the ``art`` property.
    """

    @property
    def png_data(self):
        if not self._png_data:
            image_file = os.path.join(_common.RSRC, b'image-2x3.png')
            with open(image_file, 'rb') as f:
                self._png_data = f.read()
        return self._png_data
    _png_data = None

    @property
    def jpg_data(self):
        if not self._jpg_data:
            image_file = os.path.join(_common.RSRC, b'image-2x3.jpg')
            with open(image_file, 'rb') as f:
                self._jpg_data = f.read()
        return self._jpg_data
    _jpg_data = None

    @property
    def tiff_data(self):
        if not self._jpg_data:
            image_file = os.path.join(_common.RSRC, b'image-2x3.tiff')
            with open(image_file, 'rb') as f:
                self._jpg_data = f.read()
        return self._jpg_data
    _jpg_data = None

    def test_set_png_art(self):
        mediafile = self._mediafile_fixture('empty')
        mediafile.art = self.png_data
        mediafile.save()

        mediafile = MediaFile(mediafile.filename)
        self.assertEqual(mediafile.art, self.png_data)

    def test_set_jpg_art(self):
        mediafile = self._mediafile_fixture('empty')
        mediafile.art = self.jpg_data
        mediafile.save()

        mediafile = MediaFile(mediafile.filename)
        self.assertEqual(mediafile.art, self.jpg_data)

    def test_delete_art(self):
        mediafile = self._mediafile_fixture('empty')
        mediafile.art = self.jpg_data
        mediafile.save()

        mediafile = MediaFile(mediafile.filename)
        self.assertIsNotNone(mediafile.art)

        del mediafile.art
        mediafile.save()

        mediafile = MediaFile(mediafile.filename)
        self.assertIsNone(mediafile.art)


class ImageStructureTestMixin(ArtTestMixin):
    """Test reading and writing multiple image tags.

    The tests use the `image` media file fixture. The tags of these files
    include two images, on in the PNG format, the other in JPEG format. If
    the tag format supports it they also include additional metadata.
    """

    def test_read_image_structures(self):
        mediafile = self._mediafile_fixture('image')

        self.assertEqual(len(mediafile.images), 2)

        image = next(i for i in mediafile.images
                     if i.mime_type == 'image/png')
        self.assertEqual(image.data, self.png_data)
        self.assertExtendedImageAttributes(image, desc=u'album cover',
                                           type=ImageType.front)

        image = next(i for i in mediafile.images
                     if i.mime_type == 'image/jpeg')
        self.assertEqual(image.data, self.jpg_data)
        self.assertExtendedImageAttributes(image, desc=u'the artist',
                                           type=ImageType.artist)

    def test_set_image_structure(self):
        mediafile = self._mediafile_fixture('empty')
        image = Image(data=self.png_data, desc=u'album cover',
                      type=ImageType.front)
        mediafile.images = [image]
        mediafile.save()

        mediafile = MediaFile(mediafile.filename)
        self.assertEqual(len(mediafile.images), 1)

        image = mediafile.images[0]
        self.assertEqual(image.data, self.png_data)
        self.assertEqual(image.mime_type, 'image/png')
        self.assertExtendedImageAttributes(image, desc=u'album cover',
                                           type=ImageType.front)

    def test_add_image_structure(self):
        mediafile = self._mediafile_fixture('image')
        self.assertEqual(len(mediafile.images), 2)

        image = Image(data=self.png_data, desc=u'the composer',
                      type=ImageType.composer)
        mediafile.images += [image]
        mediafile.save()

        mediafile = MediaFile(mediafile.filename)
        self.assertEqual(len(mediafile.images), 3)

        images = (i for i in mediafile.images if i.desc == u'the composer')
        image = next(images, None)
        self.assertExtendedImageAttributes(
            image, desc=u'the composer', type=ImageType.composer
        )

    def test_delete_image_structures(self):
        mediafile = self._mediafile_fixture('image')
        self.assertEqual(len(mediafile.images), 2)

        del mediafile.images
        mediafile.save()

        mediafile = MediaFile(mediafile.filename)
        self.assertEqual(len(mediafile.images), 0)

    def test_guess_cover(self):
        mediafile = self._mediafile_fixture('image')
        self.assertEqual(len(mediafile.images), 2)
        cover = CoverArtField.guess_cover_image(mediafile.images)
        self.assertEqual(cover.desc, u'album cover')
        self.assertEqual(mediafile.art, cover.data)

    def assertExtendedImageAttributes(self, image, **kwargs):  # noqa
        """Ignore extended image attributes in the base tests.
        """
        pass


class ExtendedImageStructureTestMixin(ImageStructureTestMixin):
    """Checks for additional attributes in the image structure.

    Like the base `ImageStructureTestMixin`, per-format test classes
    should include this mixin to add image-related tests.
    """

    def assertExtendedImageAttributes(self, image, desc=None, type=None):  # noqa
        self.assertEqual(image.desc, desc)
        self.assertEqual(image.type, type)

    def test_add_tiff_image(self):
        mediafile = self._mediafile_fixture('image')
        self.assertEqual(len(mediafile.images), 2)

        image = Image(data=self.tiff_data, desc=u'the composer',
                      type=ImageType.composer)
        mediafile.images += [image]
        mediafile.save()

        mediafile = MediaFile(mediafile.filename)
        self.assertEqual(len(mediafile.images), 3)

        # WMA does not preserve the order, so we have to work around this
        image = list(filter(lambda i: i.mime_type == 'image/tiff',
                     mediafile.images))[0]
        self.assertExtendedImageAttributes(
            image, desc=u'the composer', type=ImageType.composer)


class LazySaveTestMixin(object):
    """Mediafile should only write changes when tags have changed
    """

    @unittest.skip(u'not yet implemented')
    def test_unmodified(self):
        mediafile = self._mediafile_fixture('full')
        mtime = self._set_past_mtime(mediafile.filename)
        self.assertEqual(os.stat(mediafile.filename).st_mtime, mtime)

        mediafile.save()
        self.assertEqual(os.stat(mediafile.filename).st_mtime, mtime)

    @unittest.skip(u'not yet implemented')
    def test_same_tag_value(self):
        mediafile = self._mediafile_fixture('full')
        mtime = self._set_past_mtime(mediafile.filename)
        self.assertEqual(os.stat(mediafile.filename).st_mtime, mtime)

        mediafile.title = mediafile.title
        mediafile.save()
        self.assertEqual(os.stat(mediafile.filename).st_mtime, mtime)

    def test_update_same_tag_value(self):
        mediafile = self._mediafile_fixture('full')
        mtime = self._set_past_mtime(mediafile.filename)
        self.assertEqual(os.stat(mediafile.filename).st_mtime, mtime)

        mediafile.update({'title': mediafile.title})
        mediafile.save()
        self.assertEqual(os.stat(mediafile.filename).st_mtime, mtime)

    @unittest.skip(u'not yet implemented')
    def test_tag_value_change(self):
        mediafile = self._mediafile_fixture('full')
        mtime = self._set_past_mtime(mediafile.filename)
        self.assertEqual(os.stat(mediafile.filename).st_mtime, mtime)

        mediafile.title = mediafile.title
        mediafile.album = u'another'
        mediafile.save()
        self.assertNotEqual(os.stat(mediafile.filename).st_mtime, mtime)

    def test_update_changed_tag_value(self):
        mediafile = self._mediafile_fixture('full')
        mtime = self._set_past_mtime(mediafile.filename)
        self.assertEqual(os.stat(mediafile.filename).st_mtime, mtime)

        mediafile.update({'title': mediafile.title, 'album': u'another'})
        mediafile.save()
        self.assertNotEqual(os.stat(mediafile.filename).st_mtime, mtime)

    def _set_past_mtime(self, path):
        mtime = round(time.time() - 10000)
        os.utime(path, (mtime, mtime))
        return mtime


class GenreListTestMixin(object):
    """Tests access to the ``genres`` property as a list.
    """

    def test_read_genre_list(self):
        mediafile = self._mediafile_fixture('full')
        assertCountEqual(self, mediafile.genres, ['the genre'])

    def test_write_genre_list(self):
        mediafile = self._mediafile_fixture('empty')
        mediafile.genres = [u'one', u'two']
        mediafile.save()

        mediafile = MediaFile(mediafile.filename)
        assertCountEqual(self, mediafile.genres, [u'one', u'two'])

    def test_write_genre_list_get_first(self):
        mediafile = self._mediafile_fixture('empty')
        mediafile.genres = [u'one', u'two']
        mediafile.save()

        mediafile = MediaFile(mediafile.filename)
        self.assertEqual(mediafile.genre, u'one')

    def test_append_genre_list(self):
        mediafile = self._mediafile_fixture('full')
        self.assertEqual(mediafile.genre, u'the genre')
        mediafile.genres += [u'another']
        mediafile.save()

        mediafile = MediaFile(mediafile.filename)
        assertCountEqual(self, mediafile.genres, [u'the genre', u'another'])


class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin,
                        _common.TempDirMixin):
    """Test writing and reading tags. Subclasses must set ``extension``
    and ``audio_properties``.

    The basic tests for all audio formats encompass three files provided
    in our `rsrc` folder: `full.*`, `empty.*`, and `unparseable.*`.
    Respectively, they should contain a full slate of common fields
    listed in `full_initial_tags` below; no fields contents at all; and
    an unparseable release date field.

    To add support for a new file format to MediaFile, add these three
    files and then create a `ReadWriteTestBase` subclass by copying n'
    pasting one of the existing subclasses below. You will want to
    update the `format` field in that subclass, and you will probably
    need to fiddle with the `bitrate` and other format-specific fields.

    You can also add image tests (using an additional `image.*` fixture
    file) by including one of the image-related mixins.
    """

    full_initial_tags = {
        'title':             u'full',
        'artist':            u'the artist',
        'album':             u'the album',
        'genre':             u'the genre',
        'composer':          u'the composer',
        'grouping':          u'the grouping',
        'year':              2001,
        'month':             None,
        'day':               None,
        'date':              datetime.date(2001, 1, 1),
        'track':             2,
        'tracktotal':        3,
        'disc':              4,
        'disctotal':         5,
        'lyrics':            u'the lyrics',
        'comments':          u'the comments',
        'bpm':               6,
        'comp':              True,
        'mb_trackid':        '8b882575-08a5-4452-a7a7-cbb8a1531f9e',
        'mb_releasetrackid': 'c29f3a57-b439-46fd-a2e2-93776b1371e0',
        'mb_albumid':        '9e873859-8aa4-4790-b985-5a953e8ef628',
        'mb_artistid':       '7cf0ea9d-86b9-4dad-ba9e-2355a64899ea',
        'art':               None,
        'label':             u'the label',
    }

    tag_fields = [
        'title',
        'artist',
        'album',
        'genre',
        'lyricist',
        'composer',
        'composer_sort',
        'arranger',
        'grouping',
        'year',
        'month',
        'day',
        'date',
        'track',
        'tracktotal',
        'disc',
        'disctotal',
        'lyrics',
        'comments',
        'copyright',
        'bpm',
        'comp',
        'mb_trackid',
        'mb_releasetrackid',
        'mb_workid',
        'mb_albumid',
        'mb_artistid',
        'art',
        'label',
        'rg_track_peak',
        'rg_track_gain',
        'rg_album_peak',
        'rg_album_gain',
        'r128_track_gain',
        'r128_album_gain',
        'albumartist',
        'mb_albumartistid',
        'artist_sort',
        'albumartist_sort',
        'acoustid_fingerprint',
        'acoustid_id',
        'mb_releasegroupid',
        'asin',
        'catalognum',
        'barcode',
        'isrc',
        'disctitle',
        'script',
        'language',
        'country',
        'albumstatus',
        'media',
        'albumdisambig',
        'artist_credit',
        'albumartist_credit',
        'original_year',
        'original_month',
        'original_day',
        'original_date',
        'initial_key',
    ]

    def setUp(self):
        self.create_temp_dir()

    def tearDown(self):
        self.remove_temp_dir()

    def test_read_nonexisting(self):
        mediafile = self._mediafile_fixture('full')
        os.remove(mediafile.filename)
        self.assertRaises(UnreadableFileError, MediaFile, mediafile.filename)

    def test_save_nonexisting(self):
        mediafile = self._mediafile_fixture('full')
        os.remove(mediafile.filename)
        try:
            mediafile.save()
        except UnreadableFileError:
            pass

    def test_delete_nonexisting(self):
        mediafile = self._mediafile_fixture('full')
        os.remove(mediafile.filename)
        try:
            mediafile.delete()
        except UnreadableFileError:
            pass

    def test_read_audio_properties(self):
        mediafile = self._mediafile_fixture('full')
        for key, value in self.audio_properties.items():
            if isinstance(value, float):
                self.assertAlmostEqual(getattr(mediafile, key), value,
                                       delta=0.1)
            else:
                self.assertEqual(getattr(mediafile, key), value)

    def test_read_full(self):
        mediafile = self._mediafile_fixture('full')
        self.assertTags(mediafile, self.full_initial_tags)

    def test_read_empty(self):
        mediafile = self._mediafile_fixture('empty')
        for field in self.tag_fields:
            value = getattr(mediafile, field)
            if isinstance(value, list):
                assert not value
            else:
                self.assertIsNone(value)

    def test_write_empty(self):
        mediafile = self._mediafile_fixture('empty')
        tags = self._generate_tags()

        for key, value in tags.items():
            setattr(mediafile, key, value)
        mediafile.save()

        mediafile = MediaFile(mediafile.filename)
        self.assertTags(mediafile, tags)

    def test_update_empty(self):
        mediafile = self._mediafile_fixture('empty')
        tags = self._generate_tags()

        mediafile.update(tags)
        mediafile.save()

        mediafile = MediaFile(mediafile.filename)
        self.assertTags(mediafile, tags)

    def test_overwrite_full(self):
        mediafile = self._mediafile_fixture('full')
        tags = self._generate_tags()

        for key, value in tags.items():
            setattr(mediafile, key, value)
        mediafile.save()

        # Make sure the tags are already set when writing a second time
        for key, value in tags.items():
            setattr(mediafile, key, value)
        mediafile.save()

        mediafile = MediaFile(mediafile.filename)
        self.assertTags(mediafile, tags)

    def test_update_full(self):
        mediafile = self._mediafile_fixture('full')
        tags = self._generate_tags()

        mediafile.update(tags)
        mediafile.save()
        # Make sure the tags are already set when writing a second time
        mediafile.update(tags)
        mediafile.save()

        mediafile = MediaFile(mediafile.filename)
        self.assertTags(mediafile, tags)

    def test_write_date_components(self):
        mediafile = self._mediafile_fixture('full')
        mediafile.year = 2001
        mediafile.month = 1
        mediafile.day = 2
        mediafile.original_year = 1999
        mediafile.original_month = 12
        mediafile.original_day = 30
        mediafile.save()

        mediafile = MediaFile(mediafile.filename)
        self.assertEqual(mediafile.year, 2001)
        self.assertEqual(mediafile.month, 1)
        self.assertEqual(mediafile.day, 2)
        self.assertEqual(mediafile.date, datetime.date(2001, 1, 2))
        self.assertEqual(mediafile.original_year, 1999)
        self.assertEqual(mediafile.original_month, 12)
        self.assertEqual(mediafile.original_day, 30)
        self.assertEqual(mediafile.original_date, datetime.date(1999, 12, 30))

    def test_write_incomplete_date_components(self):
        mediafile = self._mediafile_fixture('empty')
        mediafile.year = 2001
        mediafile.month = None
        mediafile.day = 2
        mediafile.save()

        mediafile = MediaFile(mediafile.filename)
        self.assertEqual(mediafile.year, 2001)
        self.assertIsNone(mediafile.month)
        self.assertIsNone(mediafile.day)
        self.assertEqual(mediafile.date, datetime.date(2001, 1, 1))

    def test_write_dates(self):
        mediafile = self._mediafile_fixture('full')
        mediafile.date = datetime.date(2001, 1, 2)
        mediafile.original_date = datetime.date(1999, 12, 30)
        mediafile.save()

        mediafile = MediaFile(mediafile.filename)
        self.assertEqual(mediafile.year, 2001)
        self.assertEqual(mediafile.month, 1)
        self.assertEqual(mediafile.day, 2)
        self.assertEqual(mediafile.date, datetime.date(2001, 1, 2))
        self.assertEqual(mediafile.original_year, 1999)
        self.assertEqual(mediafile.original_month, 12)
        self.assertEqual(mediafile.original_day, 30)
        self.assertEqual(mediafile.original_date, datetime.date(1999, 12, 30))

    def test_r128_gain_stored_as_q8_number(self):
        def round_trip(x):
            q_num = round(x * pow(2, 8))
            return q_num / pow(2, 8)

        mediafile = self._mediafile_fixture('full')

        track = -1.1
        self.assertNotEqual(track, round_trip(track))
        mediafile.r128_track_gain = track
        self.assertEqual(mediafile.r128_track_gain, round_trip(track))

        album = 4.2
        self.assertNotEqual(album, round_trip(album))
        mediafile.r128_album_gain = album
        self.assertEqual(mediafile.r128_album_gain, round_trip(album))

    def test_write_packed(self):
        mediafile = self._mediafile_fixture('empty')

        mediafile.tracktotal = 2
        mediafile.track = 1
        mediafile.save()

        mediafile = MediaFile(mediafile.filename)
        self.assertEqual(mediafile.track, 1)
        self.assertEqual(mediafile.tracktotal, 2)

    def test_write_counters_without_total(self):
        mediafile = self._mediafile_fixture('full')
        self.assertEqual(mediafile.track, 2)
        self.assertEqual(mediafile.tracktotal, 3)
        self.assertEqual(mediafile.disc, 4)
        self.assertEqual(mediafile.disctotal, 5)

        mediafile.track = 10
        delattr(mediafile, 'tracktotal')
        mediafile.disc = 10
        delattr(mediafile, 'disctotal')
        mediafile.save()

        mediafile = MediaFile(mediafile.filename)
        self.assertEqual(mediafile.track, 10)
        self.assertEqual(mediafile.tracktotal, None)
        self.assertEqual(mediafile.disc, 10)
        self.assertEqual(mediafile.disctotal, None)

    def test_unparseable_date(self):
        """The `unparseable.*` fixture should not crash but should return None
        for all parts of the release date.
        """
        mediafile = self._mediafile_fixture('unparseable')

        self.assertIsNone(mediafile.date)
        self.assertIsNone(mediafile.year)
        self.assertIsNone(mediafile.month)
        self.assertIsNone(mediafile.day)

    def test_delete_tag(self):
        mediafile = self._mediafile_fixture('full')

        keys = self.full_initial_tags.keys()
        for key in set(keys) - set(['art', 'month', 'day']):
            self.assertIsNotNone(getattr(mediafile, key))
        for key in keys:
            delattr(mediafile, key)
        mediafile.save()
        mediafile = MediaFile(mediafile.filename)

        for key in keys:
            value = getattr(mediafile, key)
            if isinstance(value, list):
                assert not value
            else:
                self.assertIsNone(value)

    def test_delete_packed_total(self):
        mediafile = self._mediafile_fixture('full')

        delattr(mediafile, 'tracktotal')
        delattr(mediafile, 'disctotal')

        mediafile.save()
        mediafile = MediaFile(mediafile.filename)
        self.assertEqual(mediafile.track, self.full_initial_tags['track'])
        self.assertEqual(mediafile.disc, self.full_initial_tags['disc'])

    def test_delete_partial_date(self):
        mediafile = self._mediafile_fixture('empty')

        mediafile.date = datetime.date(2001, 12, 3)
        mediafile.save()
        mediafile = MediaFile(mediafile.filename)
        self.assertIsNotNone(mediafile.date)
        self.assertIsNotNone(mediafile.year)
        self.assertIsNotNone(mediafile.month)
        self.assertIsNotNone(mediafile.day)

        delattr(mediafile, 'month')
        mediafile.save()
        mediafile = MediaFile(mediafile.filename)
        self.assertIsNotNone(mediafile.date)
        self.assertIsNotNone(mediafile.year)
        self.assertIsNone(mediafile.month)
        self.assertIsNone(mediafile.day)

    def test_delete_year(self):
        mediafile = self._mediafile_fixture('full')

        self.assertIsNotNone(mediafile.date)
        self.assertIsNotNone(mediafile.year)

        delattr(mediafile, 'year')
        mediafile.save()
        mediafile = MediaFile(mediafile.filename)
        self.assertIsNone(mediafile.date)
        self.assertIsNone(mediafile.year)

    def assertTags(self, mediafile, tags):  # noqa
        errors = []
        for key, value in tags.items():
            try:
                value2 = getattr(mediafile, key)
            except AttributeError:
                errors.append(u'Tag %s does not exist' % key)
            else:
                if value2 != value:
                    errors.append(u'Tag %s: %r != %r' % (key, value2, value))
        if any(errors):
            errors = [u'Tags did not match'] + errors
            self.fail('\n  '.join(errors))

    def _mediafile_fixture(self, name):
        name = name + '.' + self.extension
        if not isinstance(name, bytes):
            name = name.encode('utf8')
        src = os.path.join(_common.RSRC, name)
        target = os.path.join(self.temp_dir, name)
        shutil.copy(src, target)
        return MediaFile(target)

    def _generate_tags(self, base=None):
        """Return dictionary of tags, mapping tag names to values.
        """
        tags = {}

        for key in self.tag_fields:
            if key.startswith('rg_'):
                # ReplayGain is float
                tags[key] = 1.0
            elif key.startswith('r128_'):
                # R128 is int
                tags[key] = -1
            else:
                tags[key] = 'value\u2010%s' % key

        for key in ['disc', 'disctotal', 'track', 'tracktotal', 'bpm']:
            tags[key] = 1

        for key in ['artists', 'albumartists']:
            tags[key] = ['multival', 'test']

        tags['art'] = self.jpg_data
        tags['comp'] = True

        tags['url'] = "https://example.com/"

        date = datetime.date(2001, 4, 3)
        tags['date'] = date
        tags['year'] = date.year
        tags['month'] = date.month
        tags['day'] = date.day

        original_date = datetime.date(1999, 5, 6)
        tags['original_date'] = original_date
        tags['original_year'] = original_date.year
        tags['original_month'] = original_date.month
        tags['original_day'] = original_date.day

        return tags


class PartialTestMixin(object):
    tags_without_total = {
        'track':      2,
        'tracktotal': 0,
        'disc':       4,
        'disctotal':  0,
    }

    def test_read_track_without_total(self):
        mediafile = self._mediafile_fixture('partial')
        self.assertEqual(mediafile.track, 2)
        self.assertIsNone(mediafile.tracktotal)
        self.assertEqual(mediafile.disc, 4)
        self.assertIsNone(mediafile.disctotal)


class MP3Test(ReadWriteTestBase, PartialTestMixin,
              ExtendedImageStructureTestMixin,
              unittest.TestCase):
    extension = 'mp3'
    audio_properties = {
        'length': 1.0,
        'bitrate': 80000,
        'bitrate_mode': '',
        'encoder_info': '',
        'encoder_settings': '',
        'format': 'MP3',
        'samplerate': 44100,
        'bitdepth': 0,
        'channels': 1,
    }

    def test_unknown_apic_type(self):
        mediafile = self._mediafile_fixture('image_unknown_type')
        self.assertEqual(mediafile.images[0].type, ImageType.other)

    def test_bitrate_mode(self):
        mediafile = self._mediafile_fixture('cbr')
        self.assertEqual(mediafile.bitrate_mode, 'CBR')

    def test_encoder_info(self):
        mediafile = self._mediafile_fixture('cbr')
        self.assertEqual(mediafile.encoder_info, 'LAME 3.100.0+')

    def test_encoder_settings(self):
        mediafile = self._mediafile_fixture('cbr')
        self.assertEqual(mediafile.encoder_settings, '-b 80')


class MP4Test(ReadWriteTestBase, PartialTestMixin,
              ImageStructureTestMixin, unittest.TestCase):
    extension = 'm4a'
    audio_properties = {
        'length': 1.0,
        'bitrate': 64000,
        'bitrate_mode': '',
        'encoder_info': '',
        'encoder_settings': '',
        'format': 'AAC',
        'samplerate': 44100,
        'bitdepth': 16,
        'channels': 2,
    }

    def test_add_tiff_image_fails(self):
        mediafile = self._mediafile_fixture('empty')
        with self.assertRaises(ValueError):
            mediafile.images = [Image(data=self.tiff_data)]

    def test_guess_cover(self):
        # There is no metadata associated with images, we pick one at random
        pass


class AlacTest(ReadWriteTestBase, unittest.TestCase):
    extension = 'alac.m4a'
    audio_properties = {
        'length': 1.0,
        'bitrate': 21830,
        'bitrate_mode': '',
        'encoder_info': '',
        'encoder_settings': '',
        # 'format': 'ALAC',
        'samplerate': 44100,
        'bitdepth': 16,
        'channels': 1,
    }


class MusepackTest(ReadWriteTestBase, unittest.TestCase):
    extension = 'mpc'
    audio_properties = {
        'length': 1.0,
        'bitrate': 24023,
        'bitrate_mode': '',
        'encoder_info': '',
        'encoder_settings': '',
        'format': u'Musepack',
        'samplerate': 44100,
        'bitdepth': 0,
        'channels': 2,
    }


class WMATest(ReadWriteTestBase, ExtendedImageStructureTestMixin,
              unittest.TestCase):
    extension = 'wma'
    audio_properties = {
        'length': 1.0,
        'bitrate': 128000,
        'bitrate_mode': '',
        'encoder_info': '',
        'encoder_settings': '',
        'format': u'Windows Media',
        'samplerate': 44100,
        'bitdepth': 0,
        'channels': 1,
    }

    def test_write_genre_list_get_first(self):
        # WMA does not preserve list order
        mediafile = self._mediafile_fixture('empty')
        mediafile.genres = [u'one', u'two']
        mediafile.save()

        mediafile = MediaFile(mediafile.filename)
        self.assertIn(mediafile.genre, [u'one', u'two'])

    def test_read_pure_tags(self):
        mediafile = self._mediafile_fixture('pure')
        self.assertEqual(mediafile.comments, u'the comments')
        self.assertEqual(mediafile.title, u'the title')
        self.assertEqual(mediafile.artist, u'the artist')


class OggTest(ReadWriteTestBase, ExtendedImageStructureTestMixin,
              unittest.TestCase):
    extension = 'ogg'
    audio_properties = {
        'length': 1.0,
        'bitrate': 48000,
        'bitrate_mode': '',
        'encoder_info': '',
        'encoder_settings': '',
        'format': u'OGG',
        'samplerate': 44100,
        'bitdepth': 0,
        'channels': 1,
    }

    def test_read_date_from_year_tag(self):
        mediafile = self._mediafile_fixture('year')
        self.assertEqual(mediafile.year, 2000)
        self.assertEqual(mediafile.date, datetime.date(2000, 1, 1))

    def test_write_date_to_year_tag(self):
        mediafile = self._mediafile_fixture('empty')
        mediafile.year = 2000
        mediafile.save()

        mediafile = MediaFile(mediafile.filename)
        self.assertEqual(mediafile.mgfile['YEAR'], [u'2000'])

    def test_legacy_coverart_tag(self):
        mediafile = self._mediafile_fixture('coverart')
        self.assertTrue('coverart' in mediafile.mgfile)
        self.assertEqual(mediafile.art, self.png_data)

        mediafile.art = self.png_data
        mediafile.save()

        mediafile = MediaFile(mediafile.filename)
        self.assertFalse('coverart' in mediafile.mgfile)

    def test_date_tag_with_slashes(self):
        mediafile = self._mediafile_fixture('date_with_slashes')
        self.assertEqual(mediafile.year, 2005)
        self.assertEqual(mediafile.month, 6)
        self.assertEqual(mediafile.day, 5)


class FlacTest(ReadWriteTestBase, PartialTestMixin,
               ExtendedImageStructureTestMixin,
               unittest.TestCase):
    extension = 'flac'
    audio_properties = {
        'length': 1.0,
        'bitrate': 108688,
        'bitrate_mode': '',
        'encoder_info': '',
        'encoder_settings': '',
        'format': u'FLAC',
        'samplerate': 44100,
        'bitdepth': 16,
        'channels': 1,
    }


class ApeTest(ReadWriteTestBase, ExtendedImageStructureTestMixin,
              unittest.TestCase):
    extension = 'ape'
    audio_properties = {
        'length': 1.0,
        'bitrate': 112608,
        'bitrate_mode': '',
        'encoder_info': '',
        'encoder_settings': '',
        'format': u'APE',
        'samplerate': 44100,
        'bitdepth': 16,
        'channels': 1,
    }


class WavpackTest(ReadWriteTestBase, unittest.TestCase):
    extension = 'wv'
    audio_properties = {
        'length': 1.0,
        'bitrate': 109312,
        'bitrate_mode': '',
        'encoder_info': '',
        'encoder_settings': '',
        'format': u'WavPack',
        'samplerate': 44100,
        'bitdepth': 16 if mutagen.version >= (1, 45, 0) else 0,
        'channels': 1,
    }


class OpusTest(ReadWriteTestBase, unittest.TestCase):
    extension = 'opus'
    audio_properties = {
        'length': 1.0,
        'bitrate': 66792,
        'bitrate_mode': '',
        'encoder_info': '',
        'encoder_settings': '',
        'format': u'Opus',
        'samplerate': 48000,
        'bitdepth': 0,
        'channels': 1,
    }


class AIFFTest(ReadWriteTestBase, unittest.TestCase):
    extension = 'aiff'
    audio_properties = {
        'length': 1.0,
        'bitrate': 705600,
        'bitrate_mode': '',
        'encoder_info': '',
        'encoder_settings': '',
        'format': u'AIFF',
        'samplerate': 44100,
        'bitdepth': 16,
        'channels': 1,
    }


class WAVETest(ReadWriteTestBase, unittest.TestCase):
    extension = 'wav'
    audio_properties = {
        'length': 1.0,
        'bitrate': 705600,
        'bitrate_mode': '',
        'encoder_info': '',
        'encoder_settings': '',
        'format': u'WAVE',
        'samplerate': 44100,
        'bitdepth': 16,
        'channels': 1,
    }

    full_initial_tags = {
        'title':             u'full',
        'artist':            u'the artist',
        'album':             u'the album',
        'genre':             u'the genre',
        'track':             2,
        'tracktotal':        3,
    }

    tag_fields = [
        'title',
        'artist',
        'album',
        'genre',
        'track',
        'original_year',
        'original_month',
        'original_day',
        'original_date',
    ]

    # Only a small subset of fields are supported by LIST/INFO
    # metadata format in WAVE, so some fields have been removed
    # from the inherited test cases below. Concerned fields are
    # commented above each test case.

    # Missing fields: disc, disctotal
    def test_write_counters_without_total(self):
        mediafile = self._mediafile_fixture('full')
        self.assertEqual(mediafile.track, 2)
        self.assertEqual(mediafile.tracktotal, 3)

    # Missing fields: date, year
    def test_delete_year(self):
        mediafile = self._mediafile_fixture('full')

        self.assertIsNotNone(mediafile.original_year)

        delattr(mediafile, 'original_year')
        mediafile.save()
        mediafile = MediaFile(mediafile.filename)
        self.assertIsNone(mediafile.original_year)

    # Missing fields:  disctotal
    def test_delete_packed_total(self):
        mediafile = self._mediafile_fixture('full')

        delattr(mediafile, 'tracktotal')

        mediafile.save()
        mediafile = MediaFile(mediafile.filename)
        self.assertEqual(mediafile.track, self.full_initial_tags['track'])


# Check whether we have a Mutagen version with DSF support. We can
# remove this once we require a version that includes the feature.
try:
    import mutagen.dsf  # noqa
except ImportError:
    HAVE_DSF = False
else:
    HAVE_DSF = True


@unittest.skipIf(not HAVE_DSF, "Mutagen does not have DSF support")
class DSFTest(ReadWriteTestBase, unittest.TestCase):
    extension = 'dsf'
    audio_properties = {
        'length': 0.01,
        'bitrate': 11289600,
        'bitrate_mode': '',
        'encoder_info': '',
        'encoder_settings': '',
        'format': u'DSD Stream File',
        'samplerate': 5644800,
        'bitdepth': 1,
        'channels': 2,
    }


class MediaFieldTest(unittest.TestCase):

    def test_properties_from_fields(self):
        path = os.path.join(_common.RSRC, b'full.mp3')
        mediafile = MediaFile(path)
        for field in MediaFile.fields():
            self.assertTrue(hasattr(mediafile, field))

    def test_properties_from_readable_fields(self):
        path = os.path.join(_common.RSRC, b'full.mp3')
        mediafile = MediaFile(path)
        for field in MediaFile.readable_fields():
            self.assertTrue(hasattr(mediafile, field))

    def test_known_fields(self):
        fields = list(ReadWriteTestBase.tag_fields)
        fields.extend(
            ('encoder', 'images', 'genres', 'albumtype', 'artists',
             'albumartists', 'url', 'mb_artistids', 'mb_albumartistids',
             'albumtypes', 'catalognums', 'languages')
        )
        assertCountEqual(self, MediaFile.fields(), fields)

    def test_fields_in_readable_fields(self):
        readable = MediaFile.readable_fields()
        for field in MediaFile.fields():
            self.assertIn(field, readable)


def suite():
    return unittest.TestLoader().loadTestsFromName(__name__)


if __name__ == '__main__':
    unittest.main(defaultTest='suite')
