 /***************************************************************************
    copyright           : (C) 2007 by Lukas Lalinsky
    email               : lukas@oxygene.sk
 ***************************************************************************/

/***************************************************************************
 *   This library is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU Lesser General Public License version   *
 *   2.1 as published by the Free Software Foundation.                     *
 *                                                                         *
 *   This library 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     *
 *   Lesser General Public License for more details.                       *
 *                                                                         *
 *   You should have received a copy of the GNU Lesser General Public      *
 *   License along with this library; if not, write to the Free Software   *
 *   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA         *
 *   02110-1301  USA                                                       *
 *                                                                         *
 *   Alternatively, this file is available under the Mozilla Public        *
 *   License Version 1.1.  You may obtain a copy of the License at         *
 *   http://www.mozilla.org/MPL/                                           *
 ***************************************************************************/

#include <string>
#include <stdio.h>
#include <id3v2tag.h>
#include <mpegfile.h>
#include <id3v2frame.h>
#include <uniquefileidentifierframe.h>
#include <textidentificationframe.h>
#include <attachedpictureframe.h>
#include <unsynchronizedlyricsframe.h>
#include <synchronizedlyricsframe.h>
#include <eventtimingcodesframe.h>
#include <generalencapsulatedobjectframe.h>
#include <relativevolumeframe.h>
#include <popularimeterframe.h>
#include <urllinkframe.h>
#include <ownershipframe.h>
#include <unknownframe.h>
#include <chapterframe.h>
#include <tableofcontentsframe.h>
#include <commentsframe.h>
#include <podcastframe.h>
#include <privateframe.h>
#include <tdebug.h>
#include <tpropertymap.h>
#include <tzlib.h>
#include <cppunit/extensions/HelperMacros.h>
#include "plainfile.h"
#include "utils.h"

using namespace std;
using namespace TagLib;

class PublicFrame : public ID3v2::Frame
{
  public:
    PublicFrame() : ID3v2::Frame(ByteVector("XXXX\0\0\0\0\0\0", 10)) {}
    String readStringField(const ByteVector &data, String::Type encoding,
                           int *position = 0)
      { return ID3v2::Frame::readStringField(data, encoding, position); }
    virtual String toString() const { return String(); }
    virtual void parseFields(const ByteVector &) {}
    virtual ByteVector renderFields() const { return ByteVector(); }
};

class TestID3v2 : public CppUnit::TestFixture
{
  CPPUNIT_TEST_SUITE(TestID3v2);
  CPPUNIT_TEST(testUnsynchDecode);
  CPPUNIT_TEST(testDowngradeUTF8ForID3v23_1);
  CPPUNIT_TEST(testDowngradeUTF8ForID3v23_2);
  CPPUNIT_TEST(testUTF16BEDelimiter);
  CPPUNIT_TEST(testUTF16Delimiter);
  CPPUNIT_TEST(testReadStringField);
  CPPUNIT_TEST(testParseAPIC);
  CPPUNIT_TEST(testParseAPIC_UTF16_BOM);
  CPPUNIT_TEST(testParseAPICv22);
  CPPUNIT_TEST(testRenderAPIC);
  CPPUNIT_TEST(testDontRender22);
  CPPUNIT_TEST(testParseGEOB);
  CPPUNIT_TEST(testRenderGEOB);
  CPPUNIT_TEST(testPOPMtoString);
  CPPUNIT_TEST(testParsePOPM);
  CPPUNIT_TEST(testParsePOPMWithoutCounter);
  CPPUNIT_TEST(testRenderPOPM);
  CPPUNIT_TEST(testPOPMFromFile);
  CPPUNIT_TEST(testParseRelativeVolumeFrame);
  CPPUNIT_TEST(testRenderRelativeVolumeFrame);
  CPPUNIT_TEST(testParseUniqueFileIdentifierFrame);
  CPPUNIT_TEST(testParseEmptyUniqueFileIdentifierFrame);
  CPPUNIT_TEST(testRenderUniqueFileIdentifierFrame);
  CPPUNIT_TEST(testBrokenFrame1);
  CPPUNIT_TEST(testItunes24FrameSize);
  CPPUNIT_TEST(testParseUrlLinkFrame);
  CPPUNIT_TEST(testRenderUrlLinkFrame);
  CPPUNIT_TEST(testParseUserUrlLinkFrame);
  CPPUNIT_TEST(testRenderUserUrlLinkFrame);
  CPPUNIT_TEST(testParseOwnershipFrame);
  CPPUNIT_TEST(testRenderOwnershipFrame);
  CPPUNIT_TEST(testParseSynchronizedLyricsFrame);
  CPPUNIT_TEST(testParseSynchronizedLyricsFrameWithEmptyDescritpion);
  CPPUNIT_TEST(testRenderSynchronizedLyricsFrame);
  CPPUNIT_TEST(testParseEventTimingCodesFrame);
  CPPUNIT_TEST(testRenderEventTimingCodesFrame);
  CPPUNIT_TEST(testParseCommentsFrame);
  CPPUNIT_TEST(testRenderCommentsFrame);
  CPPUNIT_TEST(testParsePodcastFrame);
  CPPUNIT_TEST(testRenderPodcastFrame);
  CPPUNIT_TEST(testParsePrivateFrame);
  CPPUNIT_TEST(testRenderPrivateFrame);
  CPPUNIT_TEST(testSaveUTF16Comment);
  CPPUNIT_TEST(testUpdateGenre23_1);
  CPPUNIT_TEST(testUpdateGenre23_2);
  CPPUNIT_TEST(testUpdateGenre23_3);
  CPPUNIT_TEST(testUpdateGenre24);
  CPPUNIT_TEST(testUpdateDate22);
  CPPUNIT_TEST(testDowngradeTo23);
  // CPPUNIT_TEST(testUpdateFullDate22); TODO TYE+TDA should be upgraded to TDRC together
  CPPUNIT_TEST(testCompressedFrameWithBrokenLength);
  CPPUNIT_TEST(testW000);
  CPPUNIT_TEST(testPropertyInterface);
  CPPUNIT_TEST(testPropertyInterface2);
  CPPUNIT_TEST(testPropertiesMovement);
  CPPUNIT_TEST(testPropertyGrouping);
  CPPUNIT_TEST(testDeleteFrame);
  CPPUNIT_TEST(testSaveAndStripID3v1ShouldNotAddFrameFromID3v1ToId3v2);
  CPPUNIT_TEST(testParseChapterFrame);
  CPPUNIT_TEST(testRenderChapterFrame);
  CPPUNIT_TEST(testParseTableOfContentsFrame);
  CPPUNIT_TEST(testRenderTableOfContentsFrame);
  CPPUNIT_TEST(testShrinkPadding);
  CPPUNIT_TEST(testEmptyFrame);
  CPPUNIT_TEST(testDuplicateTags);
  CPPUNIT_TEST(testParseTOCFrameWithManyChildren);
  CPPUNIT_TEST_SUITE_END();

public:

  void testUnsynchDecode()
  {
    MPEG::File f(TEST_FILE_PATH_C("unsynch.id3"), false);
    CPPUNIT_ASSERT(f.tag());
    CPPUNIT_ASSERT_EQUAL(String("My babe just cares for me"), f.tag()->title());
  }

  void testDowngradeUTF8ForID3v23_1()
  {
    ScopedFileCopy copy("xing", ".mp3");
    string newname = copy.fileName();

    ID3v2::TextIdentificationFrame *f
      = new ID3v2::TextIdentificationFrame(ByteVector("TPE1"), String::UTF8);
    StringList sl;
    sl.append("Foo");
    f->setText(sl);

    MPEG::File file(newname.c_str());
    file.ID3v2Tag(true)->addFrame(f);
    file.save(MPEG::File::ID3v2, File::StripOthers, ID3v2::v3);
    CPPUNIT_ASSERT_EQUAL(true, file.hasID3v2Tag());

    ByteVector data = f->render();
    CPPUNIT_ASSERT_EQUAL((unsigned int)(4+4+2+1+6+2), data.size());

    ID3v2::TextIdentificationFrame f2(data);
    CPPUNIT_ASSERT_EQUAL(sl, f2.fieldList());
    CPPUNIT_ASSERT_EQUAL(String::UTF16, f2.textEncoding());
  }

  void testDowngradeUTF8ForID3v23_2()
  {
    ScopedFileCopy copy("xing", ".mp3");

    ID3v2::UnsynchronizedLyricsFrame *f
      = new ID3v2::UnsynchronizedLyricsFrame(String::UTF8);
    f->setText("Foo");

    MPEG::File file(copy.fileName().c_str());
    file.ID3v2Tag(true)->addFrame(f);
    file.save(MPEG::File::ID3v2, File::StripOthers, ID3v2::v3);
    CPPUNIT_ASSERT(file.hasID3v2Tag());

    ByteVector data = f->render();
    CPPUNIT_ASSERT_EQUAL((unsigned int)(4+4+2+1+3+2+2+6+2), data.size());

    ID3v2::UnsynchronizedLyricsFrame f2(data);
    CPPUNIT_ASSERT_EQUAL(String("Foo"), f2.text());
    CPPUNIT_ASSERT_EQUAL(String::UTF16, f2.textEncoding());
  }

  void testUTF16BEDelimiter()
  {
    ID3v2::TextIdentificationFrame f(ByteVector("TPE1"), String::UTF16BE);
    StringList sl;
    sl.append("Foo");
    sl.append("Bar");
    f.setText(sl);
    CPPUNIT_ASSERT_EQUAL((unsigned int)(4+4+2+1+6+2+6), f.render().size());
  }

  void testUTF16Delimiter()
  {
    ID3v2::TextIdentificationFrame f(ByteVector("TPE1"), String::UTF16);
    StringList sl;
    sl.append("Foo");
    sl.append("Bar");
    f.setText(sl);
    CPPUNIT_ASSERT_EQUAL((unsigned int)(4+4+2+1+8+2+8), f.render().size());
  }

  void testBrokenFrame1()
  {
    MPEG::File f(TEST_FILE_PATH_C("broken-tenc.id3"), false);
    CPPUNIT_ASSERT(f.tag());
    CPPUNIT_ASSERT(!f.ID3v2Tag()->frameListMap().contains("TENC"));
  }

  void testReadStringField()
  {
    PublicFrame f;
    ByteVector data("abc\0", 4);
    String str = f.readStringField(data, String::Latin1);
    CPPUNIT_ASSERT_EQUAL(String("abc"), str);
  }

  // http://bugs.kde.org/show_bug.cgi?id=151078
  void testParseAPIC()
  {
    ID3v2::AttachedPictureFrame f(ByteVector("APIC"
                                             "\x00\x00\x00\x07"
                                             "\x00\x00"
                                             "\x00"
                                             "m\x00"
                                             "\x01"
                                             "d\x00"
                                             "\x00", 17));
    CPPUNIT_ASSERT_EQUAL(String("m"), f.mimeType());
    CPPUNIT_ASSERT_EQUAL(ID3v2::AttachedPictureFrame::FileIcon, f.type());
    CPPUNIT_ASSERT_EQUAL(String("d"), f.description());
  }

  void testParseAPIC_UTF16_BOM()
  {
    ID3v2::AttachedPictureFrame f(ByteVector(
      "\x41\x50\x49\x43\x00\x02\x0c\x59\x00\x00\x01\x69\x6d\x61\x67\x65"
      "\x2f\x6a\x70\x65\x67\x00\x00\xfe\xff\x00\x63\x00\x6f\x00\x76\x00"
      "\x65\x00\x72\x00\x2e\x00\x6a\x00\x70\x00\x67\x00\x00\xff\xd8\xff",
      16 * 3));
    CPPUNIT_ASSERT_EQUAL(String("image/jpeg"), f.mimeType());
    CPPUNIT_ASSERT_EQUAL(ID3v2::AttachedPictureFrame::Other, f.type());
    CPPUNIT_ASSERT_EQUAL(String("cover.jpg"), f.description());
    CPPUNIT_ASSERT_EQUAL(ByteVector("\xff\xd8\xff", 3), f.picture());
  }

  void testParseAPICv22()
  {
    ID3v2::FrameFactory *factory = ID3v2::FrameFactory::instance();
    ByteVector data = ByteVector("PIC"
                                 "\x00\x00\x08"
                                 "\x00"
                                 "JPG"
                                 "\x01"
                                 "d\x00"
                                 "\x00", 14);
    ID3v2::Header header;
    header.setMajorVersion(2);
    ID3v2::AttachedPictureFrame *frame =
      dynamic_cast<TagLib::ID3v2::AttachedPictureFrame *>(factory->createFrame(data, &header));

    CPPUNIT_ASSERT(frame);
    CPPUNIT_ASSERT_EQUAL(String("image/jpeg"), frame->mimeType());
    CPPUNIT_ASSERT_EQUAL(ID3v2::AttachedPictureFrame::FileIcon, frame->type());
    CPPUNIT_ASSERT_EQUAL(String("d"), frame->description());

    delete frame;
  }

  void testRenderAPIC()
  {
    ID3v2::AttachedPictureFrame f;
    f.setTextEncoding(String::UTF8);
    f.setMimeType("image/png");
    f.setType(ID3v2::AttachedPictureFrame::BackCover);
    f.setDescription("Description");
    f.setPicture("PNG data");
    CPPUNIT_ASSERT_EQUAL(
      ByteVector("APIC"
                 "\x00\x00\x00\x20"
                 "\x00\x00"
                 "\x03"
                 "image/png\x00"
                 "\x04"
                 "Description\x00"
                 "PNG data", 42),
      f.render());
  }

  void testDontRender22()
  {
    ID3v2::FrameFactory *factory = ID3v2::FrameFactory::instance();
    ByteVector data = ByteVector("FOO"
                                 "\x00\x00\x08"
                                 "\x00"
                                 "JPG"
                                 "\x01"
                                 "d\x00"
                                 "\x00", 14);
    ID3v2::Header header;
    header.setMajorVersion(2);
    ID3v2::UnknownFrame *frame =
      dynamic_cast<TagLib::ID3v2::UnknownFrame*>(factory->createFrame(data, &header));

    CPPUNIT_ASSERT(frame);

    ID3v2::Tag tag;
    tag.addFrame(frame);
    CPPUNIT_ASSERT_EQUAL((unsigned int)1034, tag.render().size());
  }

  // http://bugs.kde.org/show_bug.cgi?id=151078
  void testParseGEOB()
  {
    ID3v2::GeneralEncapsulatedObjectFrame f(ByteVector("GEOB"
                                             "\x00\x00\x00\x08"
                                             "\x00\x00"
                                             "\x00"
                                             "m\x00"
                                             "f\x00"
                                             "d\x00"
                                             "\x00", 18));
    CPPUNIT_ASSERT_EQUAL(String("m"), f.mimeType());
    CPPUNIT_ASSERT_EQUAL(String("f"), f.fileName());
    CPPUNIT_ASSERT_EQUAL(String("d"), f.description());
  }

  void testRenderGEOB()
  {
    ID3v2::GeneralEncapsulatedObjectFrame f;
    f.setTextEncoding(String::Latin1);
    f.setMimeType("application/octet-stream");
    f.setFileName("test.bin");
    f.setDescription("Description");
    f.setObject(ByteVector(3, '\x01'));
    CPPUNIT_ASSERT_EQUAL(
      ByteVector("GEOB"
                 "\x00\x00\x00\x32"
                 "\x00\x00"
                 "\x00"
                 "application/octet-stream\x00"
                 "test.bin\x00"
                 "Description\x00"
                 "\x01\x01\x01", 60),
      f.render());
  }

  void testParsePOPM()
  {
    ID3v2::PopularimeterFrame f(ByteVector("POPM"
                                           "\x00\x00\x00\x17"
                                           "\x00\x00"
                                           "email@example.com\x00"
                                           "\x02"
                                           "\x00\x00\x00\x03", 33));
    CPPUNIT_ASSERT_EQUAL(String("email@example.com"), f.email());
    CPPUNIT_ASSERT_EQUAL(2, f.rating());
    CPPUNIT_ASSERT_EQUAL((unsigned int)3, f.counter());
  }

  void testParsePOPMWithoutCounter()
  {
    ID3v2::PopularimeterFrame f(ByteVector("POPM"
                                           "\x00\x00\x00\x13"
                                           "\x00\x00"
                                           "email@example.com\x00"
                                           "\x02", 29));
    CPPUNIT_ASSERT_EQUAL(String("email@example.com"), f.email());
    CPPUNIT_ASSERT_EQUAL(2, f.rating());
    CPPUNIT_ASSERT_EQUAL((unsigned int)0, f.counter());
  }

  void testRenderPOPM()
  {
    ID3v2::PopularimeterFrame f;
    f.setEmail("email@example.com");
    f.setRating(2);
    f.setCounter(3);
    CPPUNIT_ASSERT_EQUAL(
      ByteVector("POPM"
                 "\x00\x00\x00\x17"
                 "\x00\x00"
                 "email@example.com\x00"
                 "\x02"
                 "\x00\x00\x00\x03", 33),
      f.render());
  }

  void testPOPMtoString()
  {
    ID3v2::PopularimeterFrame f;
    f.setEmail("email@example.com");
    f.setRating(2);
    f.setCounter(3);
    CPPUNIT_ASSERT_EQUAL(
      String("email@example.com rating=2 counter=3"), f.toString());
  }

  void testPOPMFromFile()
  {
    ScopedFileCopy copy("xing", ".mp3");
    string newname = copy.fileName();

    ID3v2::PopularimeterFrame *f = new ID3v2::PopularimeterFrame();
    f->setEmail("email@example.com");
    f->setRating(200);
    f->setCounter(3);

    {
      MPEG::File foo(newname.c_str());
      foo.ID3v2Tag()->addFrame(f);
      foo.save();
    }
    {
      MPEG::File bar(newname.c_str());
      CPPUNIT_ASSERT_EQUAL(String("email@example.com"), dynamic_cast<ID3v2::PopularimeterFrame *>(bar.ID3v2Tag()->frameList("POPM").front())->email());
      CPPUNIT_ASSERT_EQUAL(200, dynamic_cast<ID3v2::PopularimeterFrame *>(bar.ID3v2Tag()->frameList("POPM").front())->rating());
    }
  }

  // http://bugs.kde.org/show_bug.cgi?id=150481
  void testParseRelativeVolumeFrame()
  {
    ID3v2::RelativeVolumeFrame f(
      ByteVector("RVA2"              // Frame ID
                 "\x00\x00\x00\x0B"  // Frame size
                 "\x00\x00"          // Frame flags
                 "ident\x00"         // Identification
                 "\x02"              // Type of channel
                 "\x00\x0F"          // Volume adjustment
                 "\x08"              // Bits representing peak
                 "\x45", 21));       // Peak volume
    CPPUNIT_ASSERT_EQUAL(String("ident"), f.identification());
    CPPUNIT_ASSERT_EQUAL(15.0f / 512.0f,
                         f.volumeAdjustment(ID3v2::RelativeVolumeFrame::FrontRight));
    CPPUNIT_ASSERT_EQUAL(static_cast<short>(15),
                         f.volumeAdjustmentIndex(ID3v2::RelativeVolumeFrame::FrontRight));
    CPPUNIT_ASSERT_EQUAL((unsigned char)8,
                         f.peakVolume(ID3v2::RelativeVolumeFrame::FrontRight).bitsRepresentingPeak);
    CPPUNIT_ASSERT_EQUAL(ByteVector("\x45"),
                         f.peakVolume(ID3v2::RelativeVolumeFrame::FrontRight).peakVolume);
    const List<ID3v2::RelativeVolumeFrame::ChannelType> channels = f.channels();
    CPPUNIT_ASSERT_EQUAL(1U, channels.size());
    CPPUNIT_ASSERT_EQUAL(ID3v2::RelativeVolumeFrame::FrontRight, channels[0]);
  }

  void testRenderRelativeVolumeFrame()
  {
    ID3v2::RelativeVolumeFrame f;
    f.setIdentification("ident");
    f.setVolumeAdjustment(15.0f / 512.0f, ID3v2::RelativeVolumeFrame::FrontRight);
    ID3v2::RelativeVolumeFrame::PeakVolume peakVolume;
    peakVolume.bitsRepresentingPeak = 8;
    peakVolume.peakVolume.setData("\x45");
    f.setPeakVolume(peakVolume, ID3v2::RelativeVolumeFrame::FrontRight);
    CPPUNIT_ASSERT_EQUAL(
      ByteVector("RVA2"
                 "\x00\x00\x00\x0B"
                 "\x00\x00"
                 "ident\x00"
                 "\x02"
                 "\x00\x0F"
                 "\x08"
                 "\x45", 21),
      f.render());
  }

  void testParseUniqueFileIdentifierFrame()
  {
    ID3v2::UniqueFileIdentifierFrame f(
      ByteVector("UFID"                 // Frame ID
                 "\x00\x00\x00\x09"     // Frame size
                 "\x00\x00"             // Frame flags
                 "owner\x00"            // Owner identifier
                 "\x00\x01\x02", 19));  // Identifier
    CPPUNIT_ASSERT_EQUAL(String("owner"),
                         f.owner());
    CPPUNIT_ASSERT_EQUAL(ByteVector("\x00\x01\x02", 3),
                         f.identifier());
  }

  void testParseEmptyUniqueFileIdentifierFrame()
  {
    ID3v2::UniqueFileIdentifierFrame f(
      ByteVector("UFID"                 // Frame ID
                 "\x00\x00\x00\x01"     // Frame size
                 "\x00\x00"             // Frame flags
                 "\x00"                 // Owner identifier
                 "", 11));              // Identifier
    CPPUNIT_ASSERT_EQUAL(String(),
                         f.owner());
    CPPUNIT_ASSERT_EQUAL(ByteVector(),
                         f.identifier());
  }

  void testRenderUniqueFileIdentifierFrame()
  {
    ID3v2::UniqueFileIdentifierFrame f("owner", "\x01\x02\x03");
    CPPUNIT_ASSERT_EQUAL(
      ByteVector("UFID"
                 "\x00\x00\x00\x09"
                 "\x00\x00"
                 "owner\x00"
                 "\x01\x02\x03", 19),
      f.render());
  }

  void testParseUrlLinkFrame()
  {
    ID3v2::UrlLinkFrame f(
      ByteVector("WOAF"                      // Frame ID
                 "\x00\x00\x00\x12"          // Frame size
                 "\x00\x00"                  // Frame flags
                 "http://example.com", 28)); // URL
    CPPUNIT_ASSERT_EQUAL(String("http://example.com"), f.url());
  }

  void testRenderUrlLinkFrame()
  {
    ID3v2::UrlLinkFrame f("WOAF");
    f.setUrl("http://example.com");
    CPPUNIT_ASSERT_EQUAL(
      ByteVector("WOAF"                      // Frame ID
                 "\x00\x00\x00\x12"          // Frame size
                 "\x00\x00"                  // Frame flags
                 "http://example.com", 28),  // URL
      f.render());
  }

  void testParseUserUrlLinkFrame()
  {
    ID3v2::UserUrlLinkFrame f(
      ByteVector("WXXX"                      // Frame ID
                 "\x00\x00\x00\x17"          // Frame size
                 "\x00\x00"                  // Frame flags
                 "\x00"                      // Text encoding
                 "foo\x00"                   // Description
                 "http://example.com", 33)); // URL
    CPPUNIT_ASSERT_EQUAL(String("foo"), f.description());
    CPPUNIT_ASSERT_EQUAL(String("http://example.com"), f.url());
  }

  void testRenderUserUrlLinkFrame()
  {
    ID3v2::UserUrlLinkFrame f;
    f.setDescription("foo");
    f.setUrl("http://example.com");
    CPPUNIT_ASSERT_EQUAL(
      ByteVector("WXXX"                      // Frame ID
                 "\x00\x00\x00\x17"          // Frame size
                 "\x00\x00"                  // Frame flags
                 "\x00"                      // Text encoding
                 "foo\x00"                   // Description
                 "http://example.com", 33),  // URL
      f.render());
  }

  void testParseOwnershipFrame()
  {
    ID3v2::OwnershipFrame f(
                            ByteVector("OWNE"                      // Frame ID
                                       "\x00\x00\x00\x19"          // Frame size
                                       "\x00\x00"                  // Frame flags
                                       "\x00"                      // Text encoding
                                       "GBP1.99\x00"               // Price paid
                                       "20120905"                  // Date of purchase
                                       "Beatport", 35));           // Seller
    CPPUNIT_ASSERT_EQUAL(String("GBP1.99"), f.pricePaid());
    CPPUNIT_ASSERT_EQUAL(String("20120905"), f.datePurchased());
    CPPUNIT_ASSERT_EQUAL(String("Beatport"), f.seller());
  }

  void testRenderOwnershipFrame()
  {
    ID3v2::OwnershipFrame f;
    f.setPricePaid("GBP1.99");
    f.setDatePurchased("20120905");
    f.setSeller("Beatport");
    CPPUNIT_ASSERT_EQUAL(
                         ByteVector("OWNE"                      // Frame ID
                                    "\x00\x00\x00\x19"          // Frame size
                                    "\x00\x00"                  // Frame flags
                                    "\x00"                      // Text encoding
                                    "GBP1.99\x00"               // Price paid
                                    "20120905"                  // Date of purchase
                                    "Beatport", 35),  // URL
                         f.render());
  }

  void testParseSynchronizedLyricsFrame()
  {
    ID3v2::SynchronizedLyricsFrame f(
      ByteVector("SYLT"                      // Frame ID
                 "\x00\x00\x00\x21"          // Frame size
                 "\x00\x00"                  // Frame flags
                 "\x00"                      // Text encoding
                 "eng"                       // Language
                 "\x02"                      // Time stamp format
                 "\x01"                      // Content type
                 "foo\x00"                   // Content descriptor
                 "Example\x00"               // 1st text
                 "\x00\x00\x04\xd2"          // 1st time stamp
                 "Lyrics\x00"                // 2nd text
                 "\x00\x00\x11\xd7", 43));   // 2nd time stamp
    CPPUNIT_ASSERT_EQUAL(String::Latin1, f.textEncoding());
    CPPUNIT_ASSERT_EQUAL(ByteVector("eng", 3), f.language());
    CPPUNIT_ASSERT_EQUAL(ID3v2::SynchronizedLyricsFrame::AbsoluteMilliseconds,
                         f.timestampFormat());
    CPPUNIT_ASSERT_EQUAL(ID3v2::SynchronizedLyricsFrame::Lyrics, f.type());
    CPPUNIT_ASSERT_EQUAL(String("foo"), f.description());
    ID3v2::SynchronizedLyricsFrame::SynchedTextList stl = f.synchedText();
    CPPUNIT_ASSERT_EQUAL((unsigned int)2, stl.size());
    CPPUNIT_ASSERT_EQUAL(String("Example"), stl[0].text);
    CPPUNIT_ASSERT_EQUAL((unsigned int)1234, stl[0].time);
    CPPUNIT_ASSERT_EQUAL(String("Lyrics"), stl[1].text);
    CPPUNIT_ASSERT_EQUAL((unsigned int)4567, stl[1].time);
  }

  void testParseSynchronizedLyricsFrameWithEmptyDescritpion()
  {
    ID3v2::SynchronizedLyricsFrame f(
      ByteVector("SYLT"                      // Frame ID
                 "\x00\x00\x00\x21"          // Frame size
                 "\x00\x00"                  // Frame flags
                 "\x00"                      // Text encoding
                 "eng"                       // Language
                 "\x02"                      // Time stamp format
                 "\x01"                      // Content type
                 "\x00"                      // Content descriptor
                 "Example\x00"               // 1st text
                 "\x00\x00\x04\xd2"          // 1st time stamp
                 "Lyrics\x00"                // 2nd text
                 "\x00\x00\x11\xd7", 40));   // 2nd time stamp
    CPPUNIT_ASSERT_EQUAL(String::Latin1, f.textEncoding());
    CPPUNIT_ASSERT_EQUAL(ByteVector("eng", 3), f.language());
    CPPUNIT_ASSERT_EQUAL(ID3v2::SynchronizedLyricsFrame::AbsoluteMilliseconds,
                         f.timestampFormat());
    CPPUNIT_ASSERT_EQUAL(ID3v2::SynchronizedLyricsFrame::Lyrics, f.type());
    CPPUNIT_ASSERT(f.description().isEmpty());
    ID3v2::SynchronizedLyricsFrame::SynchedTextList stl = f.synchedText();
    CPPUNIT_ASSERT_EQUAL((unsigned int)2, stl.size());
    CPPUNIT_ASSERT_EQUAL(String("Example"), stl[0].text);
    CPPUNIT_ASSERT_EQUAL((unsigned int)1234, stl[0].time);
    CPPUNIT_ASSERT_EQUAL(String("Lyrics"), stl[1].text);
    CPPUNIT_ASSERT_EQUAL((unsigned int)4567, stl[1].time);
  }

  void testRenderSynchronizedLyricsFrame()
  {
    ID3v2::SynchronizedLyricsFrame f;
    f.setTextEncoding(String::Latin1);
    f.setLanguage(ByteVector("eng", 3));
    f.setTimestampFormat(ID3v2::SynchronizedLyricsFrame::AbsoluteMilliseconds);
    f.setType(ID3v2::SynchronizedLyricsFrame::Lyrics);
    f.setDescription("foo");
    ID3v2::SynchronizedLyricsFrame::SynchedTextList stl;
    stl.append(ID3v2::SynchronizedLyricsFrame::SynchedText(1234, "Example"));
    stl.append(ID3v2::SynchronizedLyricsFrame::SynchedText(4567, "Lyrics"));
    f.setSynchedText(stl);
    CPPUNIT_ASSERT_EQUAL(
      ByteVector("SYLT"                      // Frame ID
                 "\x00\x00\x00\x21"          // Frame size
                 "\x00\x00"                  // Frame flags
                 "\x00"                      // Text encoding
                 "eng"                       // Language
                 "\x02"                      // Time stamp format
                 "\x01"                      // Content type
                 "foo\x00"                   // Content descriptor
                 "Example\x00"               // 1st text
                 "\x00\x00\x04\xd2"          // 1st time stamp
                 "Lyrics\x00"                // 2nd text
                 "\x00\x00\x11\xd7", 43),    // 2nd time stamp
      f.render());
  }

  void testParseEventTimingCodesFrame()
  {
    ID3v2::EventTimingCodesFrame f(
      ByteVector("ETCO"                      // Frame ID
                 "\x00\x00\x00\x0b"          // Frame size
                 "\x00\x00"                  // Frame flags
                 "\x02"                      // Time stamp format
                 "\x02"                      // 1st event
                 "\x00\x00\xf3\x5c"          // 1st time stamp
                 "\xfe"                      // 2nd event
                 "\x00\x36\xee\x80", 21));   // 2nd time stamp
    CPPUNIT_ASSERT_EQUAL(ID3v2::EventTimingCodesFrame::AbsoluteMilliseconds,
                         f.timestampFormat());
    ID3v2::EventTimingCodesFrame::SynchedEventList sel = f.synchedEvents();
    CPPUNIT_ASSERT_EQUAL((unsigned int)2, sel.size());
    CPPUNIT_ASSERT_EQUAL(ID3v2::EventTimingCodesFrame::IntroStart, sel[0].type);
    CPPUNIT_ASSERT_EQUAL((unsigned int)62300, sel[0].time);
    CPPUNIT_ASSERT_EQUAL(ID3v2::EventTimingCodesFrame::AudioFileEnds, sel[1].type);
    CPPUNIT_ASSERT_EQUAL((unsigned int)3600000, sel[1].time);
  }

  void testRenderEventTimingCodesFrame()
  {
    ID3v2::EventTimingCodesFrame f;
    f.setTimestampFormat(ID3v2::EventTimingCodesFrame::AbsoluteMilliseconds);
    ID3v2::EventTimingCodesFrame::SynchedEventList sel;
    sel.append(ID3v2::EventTimingCodesFrame::SynchedEvent(62300, ID3v2::EventTimingCodesFrame::IntroStart));
    sel.append(ID3v2::EventTimingCodesFrame::SynchedEvent(3600000, ID3v2::EventTimingCodesFrame::AudioFileEnds));
    f.setSynchedEvents(sel);
    CPPUNIT_ASSERT_EQUAL(
      ByteVector("ETCO"                      // Frame ID
                 "\x00\x00\x00\x0b"          // Frame size
                 "\x00\x00"                  // Frame flags
                 "\x02"                      // Time stamp format
                 "\x02"                      // 1st event
                 "\x00\x00\xf3\x5c"          // 1st time stamp
                 "\xfe"                      // 2nd event
                 "\x00\x36\xee\x80", 21),    // 2nd time stamp
      f.render());
  }

  void testParseCommentsFrame()
  {
    ID3v2::CommentsFrame f(
      ByteVector("COMM"
                 "\x00\x00\x00\x14"
                 "\x00\x00"
                 "\x03"
                 "deu"
                 "Description\x00"
                 "Text", 30));
    CPPUNIT_ASSERT_EQUAL(String::UTF8, f.textEncoding());
    CPPUNIT_ASSERT_EQUAL(ByteVector("deu"), f.language());
    CPPUNIT_ASSERT_EQUAL(String("Description"), f.description());
    CPPUNIT_ASSERT_EQUAL(String("Text"), f.text());
  }

  void testRenderCommentsFrame()
  {
    ID3v2::CommentsFrame f;
    f.setTextEncoding(String::UTF16);
    f.setLanguage("eng");
    f.setDescription("Description");
    f.setText("Text");
    CPPUNIT_ASSERT_EQUAL(
      ByteVector("COMM"
                 "\x00\x00\x00\x28"
                 "\x00\x00"
                 "\x01"
                 "eng"
                 "\xff\xfe" "D\0e\0s\0c\0r\0i\0p\0t\0i\0o\0n\0" "\x00\x00"
                 "\xff\xfe" "T\0e\0x\0t\0", 50),
      f.render());
  }

  void testParsePodcastFrame()
  {
    ID3v2::FrameFactory *factory = ID3v2::FrameFactory::instance();
    ByteVector data = ByteVector("PCST"
                                 "\x00\x00\x00\x04"
                                 "\x00\x00"
                                 "\x00\x00\x00\x00", 14);
    const ID3v2::Header header;
    CPPUNIT_ASSERT(dynamic_cast<ID3v2::PodcastFrame *>(
                     factory->createFrame(data, &header)));
  }

  void testRenderPodcastFrame()
  {
    ID3v2::PodcastFrame f;
    CPPUNIT_ASSERT_EQUAL(
      ByteVector("PCST"
                 "\x00\x00\x00\x04"
                 "\x00\x00"
                 "\x00\x00\x00\x00", 14),
      f.render());
  }

  void testParsePrivateFrame()
  {
    ID3v2::PrivateFrame f(
      ByteVector("PRIV"
                 "\x00\x00\x00\x0e"
                 "\x00\x00"
                 "WM/Provider\x00"
                 "TL", 24));
    CPPUNIT_ASSERT_EQUAL(String("WM/Provider"), f.owner());
    CPPUNIT_ASSERT_EQUAL(ByteVector("TL"), f.data());
  }

  void testRenderPrivateFrame()
  {
    ID3v2::PrivateFrame f;
    f.setOwner("WM/Provider");
    f.setData("TL");
    CPPUNIT_ASSERT_EQUAL(
      ByteVector("PRIV"
                 "\x00\x00\x00\x0e"
                 "\x00\x00"
                 "WM/Provider\x00"
                 "TL", 24),
      f.render());
  }

  void testItunes24FrameSize()
  {
    MPEG::File f(TEST_FILE_PATH_C("005411.id3"), false);
    CPPUNIT_ASSERT(f.tag());
    CPPUNIT_ASSERT(f.ID3v2Tag()->frameListMap().contains("TIT2"));
    CPPUNIT_ASSERT_EQUAL(String("Sunshine Superman"), f.ID3v2Tag()->frameListMap()["TIT2"].front()->toString());
  }

  void testSaveUTF16Comment()
  {
    String::Type defaultEncoding = ID3v2::FrameFactory::instance()->defaultTextEncoding();
    ScopedFileCopy copy("xing", ".mp3");
    string newname = copy.fileName();
    ID3v2::FrameFactory::instance()->setDefaultTextEncoding(String::UTF16);
    {
      MPEG::File foo(newname.c_str());
      foo.strip();
      foo.tag()->setComment("Test comment!");
      foo.save();
    }
    {
      MPEG::File bar(newname.c_str());
      CPPUNIT_ASSERT_EQUAL(String("Test comment!"), bar.tag()->comment());
      ID3v2::FrameFactory::instance()->setDefaultTextEncoding(defaultEncoding);
    }
  }

  void testUpdateGenre23_1()
  {
    // "Refinement" is the same as the ID3v1 genre - duplicate
    ID3v2::FrameFactory *factory = ID3v2::FrameFactory::instance();
    ByteVector data = ByteVector("TCON"                 // Frame ID
                                 "\x00\x00\x00\x10"     // Frame size
                                 "\x00\x00"             // Frame flags
                                 "\x00"                 // Encoding
                                 "(22)Death Metal", 26);     // Text
    ID3v2::Header header;
    header.setMajorVersion(3);
    ID3v2::TextIdentificationFrame *frame =
      dynamic_cast<TagLib::ID3v2::TextIdentificationFrame*>(factory->createFrame(data, &header));
    CPPUNIT_ASSERT_EQUAL((unsigned int)1, frame->fieldList().size());
    CPPUNIT_ASSERT_EQUAL(String("Death Metal"), frame->fieldList()[0]);

    ID3v2::Tag tag;
    tag.addFrame(frame);
    CPPUNIT_ASSERT_EQUAL(String("Death Metal"), tag.genre());
  }

  void testUpdateGenre23_2()
  {
    // "Refinement" is different from the ID3v1 genre
    ID3v2::FrameFactory *factory = ID3v2::FrameFactory::instance();
    ByteVector data = ByteVector("TCON"                 // Frame ID
                                 "\x00\x00\x00\x0d"     // Frame size
                                 "\x00\x00"             // Frame flags
                                 "\x00"                 // Encoding
                                 "(4)Eurodisco", 23);   // Text
    ID3v2::Header header;
    header.setMajorVersion(3);
    ID3v2::TextIdentificationFrame *frame =
      dynamic_cast<TagLib::ID3v2::TextIdentificationFrame*>(factory->createFrame(data, &header));
    CPPUNIT_ASSERT_EQUAL((unsigned int)2, frame->fieldList().size());
    CPPUNIT_ASSERT_EQUAL(String("4"), frame->fieldList()[0]);
    CPPUNIT_ASSERT_EQUAL(String("Eurodisco"), frame->fieldList()[1]);

    ID3v2::Tag tag;
    tag.addFrame(frame);
    CPPUNIT_ASSERT_EQUAL(String("Disco Eurodisco"), tag.genre());
  }

  void testUpdateGenre23_3()
  {
    // Multiple references and a refinement
    ID3v2::FrameFactory *factory = ID3v2::FrameFactory::instance();
    ByteVector data = ByteVector("TCON"                 // Frame ID
                                 "\x00\x00\x00\x15"     // Frame size
                                 "\x00\x00"             // Frame flags
                                 "\x00"                 // Encoding
                                 "(9)(138)Viking Metal", 31);   // Text
    ID3v2::Header header;
    header.setMajorVersion(3);
    ID3v2::TextIdentificationFrame *frame =
      dynamic_cast<TagLib::ID3v2::TextIdentificationFrame*>(factory->createFrame(data, &header));
    CPPUNIT_ASSERT_EQUAL(3U, frame->fieldList().size());
    CPPUNIT_ASSERT_EQUAL(String("9"), frame->fieldList()[0]);
    CPPUNIT_ASSERT_EQUAL(String("138"), frame->fieldList()[1]);
    CPPUNIT_ASSERT_EQUAL(String("Viking Metal"), frame->fieldList()[2]);

    ID3v2::Tag tag;
    tag.addFrame(frame);
    CPPUNIT_ASSERT_EQUAL(String("Metal Black Metal Viking Metal"), tag.genre());
  }

  void testUpdateGenre24()
  {
    ID3v2::FrameFactory *factory = ID3v2::FrameFactory::instance();
    ByteVector data = ByteVector("TCON"                   // Frame ID
                                 "\x00\x00\x00\x0D"       // Frame size
                                 "\x00\x00"               // Frame flags
                                 "\0"                   // Encoding
                                 "14\0Eurodisco", 23);     // Text
    ID3v2::Header header;
    ID3v2::TextIdentificationFrame *frame =
      dynamic_cast<TagLib::ID3v2::TextIdentificationFrame*>(factory->createFrame(data, &header));
    CPPUNIT_ASSERT_EQUAL((unsigned int)2, frame->fieldList().size());
    CPPUNIT_ASSERT_EQUAL(String("14"), frame->fieldList()[0]);
    CPPUNIT_ASSERT_EQUAL(String("Eurodisco"), frame->fieldList()[1]);

    ID3v2::Tag tag;
    tag.addFrame(frame);
    CPPUNIT_ASSERT_EQUAL(String("R&B Eurodisco"), tag.genre());
  }

  void testUpdateDate22()
  {
    MPEG::File f(TEST_FILE_PATH_C("id3v22-tda.mp3"), false);
    CPPUNIT_ASSERT(f.tag());
    CPPUNIT_ASSERT_EQUAL((unsigned int)2010, f.tag()->year());
  }

  void testUpdateFullDate22()
  {
    MPEG::File f(TEST_FILE_PATH_C("id3v22-tda.mp3"), false);
    CPPUNIT_ASSERT(f.tag());
    CPPUNIT_ASSERT_EQUAL(String("2010-04-03"), f.ID3v2Tag()->frameListMap()["TDRC"].front()->toString());
  }

  void testDowngradeTo23()
  {
    ScopedFileCopy copy("xing", ".mp3");
    string newname = copy.fileName();

    ID3v2::TextIdentificationFrame *tf;
    {
      MPEG::File foo(newname.c_str());
      tf = new ID3v2::TextIdentificationFrame("TDOR", String::Latin1);
      tf->setText("2011-03-16");
      foo.ID3v2Tag()->addFrame(tf);
      tf = new ID3v2::TextIdentificationFrame("TDRC", String::Latin1);
      tf->setText("2012-04-17T12:01");
      foo.ID3v2Tag()->addFrame(tf);
      tf = new ID3v2::TextIdentificationFrame("TMCL", String::Latin1);
      tf->setText(StringList().append("Guitar").append("Artist 1").append("Drums").append("Artist 2"));
      foo.ID3v2Tag()->addFrame(tf);
      tf = new ID3v2::TextIdentificationFrame("TIPL", String::Latin1);
      tf->setText(StringList().append("Producer").append("Artist 3").append("Mastering").append("Artist 4"));
      foo.ID3v2Tag()->addFrame(tf);
      tf = new ID3v2::TextIdentificationFrame("TCON", String::Latin1);
      tf->setText(StringList().append("51").append("Noise").append("Power Noise"));
      foo.ID3v2Tag()->addFrame(tf);
      foo.ID3v2Tag()->addFrame(new ID3v2::TextIdentificationFrame("TDRL", String::Latin1));
      foo.ID3v2Tag()->addFrame(new ID3v2::TextIdentificationFrame("TDTG", String::Latin1));
      foo.ID3v2Tag()->addFrame(new ID3v2::TextIdentificationFrame("TMOO", String::Latin1));
      foo.ID3v2Tag()->addFrame(new ID3v2::TextIdentificationFrame("TPRO", String::Latin1));
      foo.ID3v2Tag()->addFrame(new ID3v2::TextIdentificationFrame("TSOA", String::Latin1));
      foo.ID3v2Tag()->addFrame(new ID3v2::TextIdentificationFrame("TSOT", String::Latin1));
      foo.ID3v2Tag()->addFrame(new ID3v2::TextIdentificationFrame("TSST", String::Latin1));
      foo.ID3v2Tag()->addFrame(new ID3v2::TextIdentificationFrame("TSOP", String::Latin1));
      foo.save(MPEG::File::AllTags, File::StripOthers, ID3v2::v3);
    }
    {
      MPEG::File bar(newname.c_str());
      tf = dynamic_cast<ID3v2::TextIdentificationFrame *>(bar.ID3v2Tag()->frameList("TDOR").front());
      CPPUNIT_ASSERT(tf);
      CPPUNIT_ASSERT_EQUAL((unsigned int)1, tf->fieldList().size());
      CPPUNIT_ASSERT_EQUAL(String("2011"), tf->fieldList().front());
      tf = dynamic_cast<ID3v2::TextIdentificationFrame *>(bar.ID3v2Tag()->frameList("TDRC").front());
      CPPUNIT_ASSERT(tf);
      CPPUNIT_ASSERT_EQUAL((unsigned int)1, tf->fieldList().size());
      CPPUNIT_ASSERT_EQUAL(String("2012-04-17T12:01"), tf->fieldList().front());
      tf = dynamic_cast<ID3v2::TextIdentificationFrame *>(bar.ID3v2Tag()->frameList("TIPL").front());
      CPPUNIT_ASSERT(tf);
      CPPUNIT_ASSERT_EQUAL((unsigned int)8, tf->fieldList().size());
      CPPUNIT_ASSERT_EQUAL(String("Guitar"), tf->fieldList()[0]);
      CPPUNIT_ASSERT_EQUAL(String("Artist 1"), tf->fieldList()[1]);
      CPPUNIT_ASSERT_EQUAL(String("Drums"), tf->fieldList()[2]);
      CPPUNIT_ASSERT_EQUAL(String("Artist 2"), tf->fieldList()[3]);
      CPPUNIT_ASSERT_EQUAL(String("Producer"), tf->fieldList()[4]);
      CPPUNIT_ASSERT_EQUAL(String("Artist 3"), tf->fieldList()[5]);
      CPPUNIT_ASSERT_EQUAL(String("Mastering"), tf->fieldList()[6]);
      CPPUNIT_ASSERT_EQUAL(String("Artist 4"), tf->fieldList()[7]);
      tf = dynamic_cast<ID3v2::TextIdentificationFrame *>(bar.ID3v2Tag()->frameList("TCON").front());
      CPPUNIT_ASSERT(tf);
      CPPUNIT_ASSERT_EQUAL(3U, tf->fieldList().size());
      CPPUNIT_ASSERT_EQUAL(String("51"), tf->fieldList()[0]);
      CPPUNIT_ASSERT_EQUAL(String("39"), tf->fieldList()[1]);
      CPPUNIT_ASSERT_EQUAL(String("Power Noise"), tf->fieldList()[2]);
      CPPUNIT_ASSERT(!bar.ID3v2Tag()->frameListMap().contains("TDRL"));
      CPPUNIT_ASSERT(!bar.ID3v2Tag()->frameListMap().contains("TDTG"));
      CPPUNIT_ASSERT(!bar.ID3v2Tag()->frameListMap().contains("TMOO"));
      CPPUNIT_ASSERT(!bar.ID3v2Tag()->frameListMap().contains("TPRO"));
#ifdef NO_ITUNES_HACKS
      CPPUNIT_ASSERT(!bar.ID3v2Tag()->frameListMap().contains("TSOA"));
      CPPUNIT_ASSERT(!bar.ID3v2Tag()->frameListMap().contains("TSOT"));
      CPPUNIT_ASSERT(!bar.ID3v2Tag()->frameListMap().contains("TSOP"));
#endif
      CPPUNIT_ASSERT(!bar.ID3v2Tag()->frameListMap().contains("TSST"));
    }
    {
      const ByteVector expectedId3v23Data(
            "ID3" "\x03\x00\x00\x00\x00\x09\x49"
            "TSOA" "\x00\x00\x00\x01\x00\x00\x00"
            "TSOT" "\x00\x00\x00\x01\x00\x00\x00"
            "TSOP" "\x00\x00\x00\x01\x00\x00\x00"
            "TORY" "\x00\x00\x00\x05\x00\x00\x00" "2011"
            "TYER" "\x00\x00\x00\x05\x00\x00\x00" "2012"
            "TDAT" "\x00\x00\x00\x05\x00\x00\x00" "1704"
            "TIME" "\x00\x00\x00\x05\x00\x00\x00" "1201"
            "IPLS" "\x00\x00\x00\x44\x00\x00\x00" "Guitar" "\x00"
            "Artist 1" "\x00" "Drums" "\x00" "Artist 2" "\x00" "Producer" "\x00"
            "Artist 3" "\x00" "Mastering" "\x00" "Artist 4"
            "TCON" "\x00\x00\x00\x14\x00\x00\x00" "(51)(39)Power Noise", 211);
      const ByteVector actualId3v23Data =
          PlainFile(newname.c_str()).readBlock(expectedId3v23Data.size());
      CPPUNIT_ASSERT_EQUAL(expectedId3v23Data, actualId3v23Data);
    }

    ScopedFileCopy rareFramesCopy("rare_frames", ".mp3");

    {
      MPEG::File f(rareFramesCopy.fileName().c_str());
      f.save(MPEG::File::AllTags, File::StripOthers, ID3v2::v3);
      f.seek(f.find("TCON") + 11);
      CPPUNIT_ASSERT_EQUAL(ByteVector("(13)"), f.readBlock(4));
    }
  }

  void testCompressedFrameWithBrokenLength()
  {
    MPEG::File f(TEST_FILE_PATH_C("compressed_id3_frame.mp3"), false);
    CPPUNIT_ASSERT(f.ID3v2Tag()->frameListMap().contains("APIC"));

    if(zlib::isAvailable()) {
      ID3v2::AttachedPictureFrame *frame
        = dynamic_cast<TagLib::ID3v2::AttachedPictureFrame*>(f.ID3v2Tag()->frameListMap()["APIC"].front());
      CPPUNIT_ASSERT(frame);
      CPPUNIT_ASSERT_EQUAL(String("image/bmp"), frame->mimeType());
      CPPUNIT_ASSERT_EQUAL(ID3v2::AttachedPictureFrame::Other, frame->type());
      CPPUNIT_ASSERT_EQUAL(String(""), frame->description());
      CPPUNIT_ASSERT_EQUAL((unsigned int)86414, frame->picture().size());
    }
    else {
      // Skip the test if ZLIB is not installed.
      // The message "Compressed frames are currently not supported." will be displayed.

      ID3v2::UnknownFrame *frame
        = dynamic_cast<TagLib::ID3v2::UnknownFrame*>(f.ID3v2Tag()->frameListMap()["APIC"].front());
      CPPUNIT_ASSERT(frame);
    }
  }

  void testW000()
  {
    MPEG::File f(TEST_FILE_PATH_C("w000.mp3"), false);
    CPPUNIT_ASSERT(f.ID3v2Tag()->frameListMap().contains("W000"));
    ID3v2::UrlLinkFrame *frame =
    dynamic_cast<TagLib::ID3v2::UrlLinkFrame*>(f.ID3v2Tag()->frameListMap()["W000"].front());
    CPPUNIT_ASSERT(frame);
    CPPUNIT_ASSERT_EQUAL(String("lukas.lalinsky@example.com____"), frame->url());
  }

  void testPropertyInterface()
  {
    ScopedFileCopy copy("rare_frames", ".mp3");
    string newname = copy.fileName();
    MPEG::File f(newname.c_str());
    PropertyMap dict = f.ID3v2Tag(false)->properties();
    CPPUNIT_ASSERT_EQUAL((unsigned int)6, dict.size());

    CPPUNIT_ASSERT(dict.contains("USERTEXTDESCRIPTION1"));
    CPPUNIT_ASSERT(dict.contains("QuodLibet::USERTEXTDESCRIPTION2"));
    CPPUNIT_ASSERT_EQUAL((unsigned int)2, dict["USERTEXTDESCRIPTION1"].size());
    CPPUNIT_ASSERT_EQUAL((unsigned int)2, dict["QuodLibet::USERTEXTDESCRIPTION2"].size());
    CPPUNIT_ASSERT_EQUAL(String("userTextData1"), dict["USERTEXTDESCRIPTION1"][0]);
    CPPUNIT_ASSERT_EQUAL(String("userTextData2"), dict["USERTEXTDESCRIPTION1"][1]);
    CPPUNIT_ASSERT_EQUAL(String("userTextData1"), dict["QuodLibet::USERTEXTDESCRIPTION2"][0]);
    CPPUNIT_ASSERT_EQUAL(String("userTextData2"), dict["QuodLibet::USERTEXTDESCRIPTION2"][1]);

    CPPUNIT_ASSERT_EQUAL(String("Pop"), dict["GENRE"].front());

    CPPUNIT_ASSERT_EQUAL(String("http://a.user.url"), dict["URL:USERURL"].front());

    CPPUNIT_ASSERT_EQUAL(String("http://a.user.url/with/empty/description"), dict["URL"].front());
    CPPUNIT_ASSERT_EQUAL(String("A COMMENT"), dict["COMMENT"].front());

    CPPUNIT_ASSERT_EQUAL(1u, dict.unsupportedData().size());
    CPPUNIT_ASSERT_EQUAL(String("UFID/supermihi@web.de"), dict.unsupportedData().front());
  }

  void testPropertyInterface2()
  {
    ID3v2::Tag tag;
    ID3v2::UnsynchronizedLyricsFrame *frame1 = new ID3v2::UnsynchronizedLyricsFrame();
    frame1->setDescription("test");
    frame1->setText("la-la-la test");
    tag.addFrame(frame1);

    ID3v2::UnsynchronizedLyricsFrame *frame2 = new ID3v2::UnsynchronizedLyricsFrame();
    frame2->setDescription("");
    frame2->setText("la-la-la nodescription");
    tag.addFrame(frame2);

    ID3v2::AttachedPictureFrame *frame3 = new ID3v2::AttachedPictureFrame();
    frame3->setDescription("test picture");
    tag.addFrame(frame3);

    ID3v2::TextIdentificationFrame *frame4 = new ID3v2::TextIdentificationFrame("TIPL");
    frame4->setText("single value is invalid for TIPL");
    tag.addFrame(frame4);

    ID3v2::TextIdentificationFrame *frame5 = new ID3v2::TextIdentificationFrame("TMCL");
    StringList tmclData;
    tmclData.append("VIOLIN");
    tmclData.append("a violinist");
    tmclData.append("PIANO");
    tmclData.append("a pianist");
    frame5->setText(tmclData);
    tag.addFrame(frame5);

    ID3v2::UniqueFileIdentifierFrame *frame6 = new ID3v2::UniqueFileIdentifierFrame("http://musicbrainz.org", "152454b9-19ba-49f3-9fc9-8fc26545cf41");
    tag.addFrame(frame6);

    ID3v2::UniqueFileIdentifierFrame *frame7 = new ID3v2::UniqueFileIdentifierFrame("http://example.com", "123");
    tag.addFrame(frame7);

    ID3v2::UserTextIdentificationFrame *frame8 = new ID3v2::UserTextIdentificationFrame();
    frame8->setDescription("MusicBrainz Album Id");
    frame8->setText("95c454a5-d7e0-4d8f-9900-db04aca98ab3");
    tag.addFrame(frame8);

    PropertyMap properties = tag.properties();

    CPPUNIT_ASSERT_EQUAL(3u, properties.unsupportedData().size());
    CPPUNIT_ASSERT(properties.unsupportedData().contains("TIPL"));
    CPPUNIT_ASSERT(properties.unsupportedData().contains("APIC"));
    CPPUNIT_ASSERT(properties.unsupportedData().contains("UFID/http://example.com"));

    CPPUNIT_ASSERT(properties.contains("PERFORMER:VIOLIN"));
    CPPUNIT_ASSERT(properties.contains("PERFORMER:PIANO"));
    CPPUNIT_ASSERT_EQUAL(String("a violinist"), properties["PERFORMER:VIOLIN"].front());
    CPPUNIT_ASSERT_EQUAL(String("a pianist"), properties["PERFORMER:PIANO"].front());

    CPPUNIT_ASSERT(properties.contains("LYRICS"));
    CPPUNIT_ASSERT(properties.contains("LYRICS:TEST"));

    CPPUNIT_ASSERT(properties.contains("MUSICBRAINZ_TRACKID"));
    CPPUNIT_ASSERT_EQUAL(String("152454b9-19ba-49f3-9fc9-8fc26545cf41"), properties["MUSICBRAINZ_TRACKID"].front());

    CPPUNIT_ASSERT(properties.contains("MUSICBRAINZ_ALBUMID"));
    CPPUNIT_ASSERT_EQUAL(String("95c454a5-d7e0-4d8f-9900-db04aca98ab3"), properties["MUSICBRAINZ_ALBUMID"].front());

    tag.removeUnsupportedProperties(properties.unsupportedData());
    CPPUNIT_ASSERT(tag.frameList("APIC").isEmpty());
    CPPUNIT_ASSERT(tag.frameList("TIPL").isEmpty());
    CPPUNIT_ASSERT_EQUAL((ID3v2::UniqueFileIdentifierFrame *)0, ID3v2::UniqueFileIdentifierFrame::findByOwner(&tag, "http://example.com"));
    CPPUNIT_ASSERT_EQUAL(frame6, ID3v2::UniqueFileIdentifierFrame::findByOwner(&tag, "http://musicbrainz.org"));
  }

  void testPropertiesMovement()
  {
    ID3v2::Tag tag;
    ID3v2::TextIdentificationFrame *frameMvnm = new ID3v2::TextIdentificationFrame("MVNM");
    frameMvnm->setText("Movement Name");
    tag.addFrame(frameMvnm);

    ID3v2::TextIdentificationFrame *frameMvin = new ID3v2::TextIdentificationFrame("MVIN");
    frameMvin->setText("2/3");
    tag.addFrame(frameMvin);

    PropertyMap properties = tag.properties();
    CPPUNIT_ASSERT(properties.contains("MOVEMENTNAME"));
    CPPUNIT_ASSERT(properties.contains("MOVEMENTNUMBER"));
    CPPUNIT_ASSERT_EQUAL(String("Movement Name"), properties["MOVEMENTNAME"].front());
    CPPUNIT_ASSERT_EQUAL(String("2/3"), properties["MOVEMENTNUMBER"].front());

    ByteVector frameDataMvnm("MVNM"
                             "\x00\x00\x00\x0e"
                             "\x00\x00"
                             "\x00"
                             "Movement Name", 24);
    CPPUNIT_ASSERT_EQUAL(frameDataMvnm, frameMvnm->render());
    ByteVector frameDataMvin("MVIN"
                             "\x00\x00\x00\x04"
                             "\x00\x00"
                             "\x00"
                             "2/3", 14);
    CPPUNIT_ASSERT_EQUAL(frameDataMvin, frameMvin->render());

    ID3v2::FrameFactory *factory = ID3v2::FrameFactory::instance();
    ID3v2::Header header;
    ID3v2::TextIdentificationFrame *parsedFrameMvnm =
      dynamic_cast<ID3v2::TextIdentificationFrame *>(
        factory->createFrame(frameDataMvnm, &header));
    ID3v2::TextIdentificationFrame *parsedFrameMvin =
      dynamic_cast<ID3v2::TextIdentificationFrame *>(
        factory->createFrame(frameDataMvin, &header));
    CPPUNIT_ASSERT(parsedFrameMvnm);
    CPPUNIT_ASSERT(parsedFrameMvin);
    CPPUNIT_ASSERT_EQUAL(String("Movement Name"), parsedFrameMvnm->toString());
    CPPUNIT_ASSERT_EQUAL(String("2/3"), parsedFrameMvin->toString());

    tag.addFrame(parsedFrameMvnm);
    tag.addFrame(parsedFrameMvin);
  }

  void testPropertyGrouping()
  {
    ID3v2::Tag tag;
    ID3v2::TextIdentificationFrame *frameGrp1 = new ID3v2::TextIdentificationFrame("GRP1");
    frameGrp1->setText("Grouping");
    tag.addFrame(frameGrp1);

    PropertyMap properties = tag.properties();
    CPPUNIT_ASSERT(properties.contains("GROUPING"));
    CPPUNIT_ASSERT_EQUAL(String("Grouping"), properties["GROUPING"].front());

    ByteVector frameDataGrp1("GRP1"
                             "\x00\x00\x00\x09"
                             "\x00\x00"
                             "\x00"
                             "Grouping", 19);
    CPPUNIT_ASSERT_EQUAL(frameDataGrp1, frameGrp1->render());

    ID3v2::FrameFactory *factory = ID3v2::FrameFactory::instance();
    ID3v2::Header header;
    ID3v2::TextIdentificationFrame *parsedFrameGrp1 =
      dynamic_cast<ID3v2::TextIdentificationFrame *>(
        factory->createFrame(frameDataGrp1, &header));
    CPPUNIT_ASSERT(parsedFrameGrp1);
    CPPUNIT_ASSERT_EQUAL(String("Grouping"), parsedFrameGrp1->toString());

    tag.addFrame(parsedFrameGrp1);
  }

  void testDeleteFrame()
  {
    ScopedFileCopy copy("rare_frames", ".mp3");
    string newname = copy.fileName();

    {
      MPEG::File f(newname.c_str());
      ID3v2::Tag *t = f.ID3v2Tag();
      ID3v2::Frame *frame = t->frameList("TCON")[0];
      CPPUNIT_ASSERT_EQUAL(1u, t->frameList("TCON").size());
      t->removeFrame(frame, true);
      f.save(MPEG::File::ID3v2);
    }
    {
      MPEG::File f2(newname.c_str());
      ID3v2::Tag *t = f2.ID3v2Tag();
      CPPUNIT_ASSERT(t->frameList("TCON").isEmpty());
    }
  }

  void testSaveAndStripID3v1ShouldNotAddFrameFromID3v1ToId3v2()
  {
    ScopedFileCopy copy("xing", ".mp3");
    string newname = copy.fileName();

    {
      MPEG::File foo(newname.c_str());
      foo.tag()->setArtist("Artist");
      foo.save(MPEG::File::ID3v1 | MPEG::File::ID3v2);
    }

    {
      MPEG::File bar(newname.c_str());
      bar.ID3v2Tag()->removeFrames("TPE1");
      // Should strip ID3v1 here and not add old values to ID3v2 again
      bar.save(MPEG::File::ID3v2, File::StripOthers);
    }

    MPEG::File f(newname.c_str());
    CPPUNIT_ASSERT(!f.ID3v2Tag()->frameListMap().contains("TPE1"));
  }

  void testParseChapterFrame()
  {
    ID3v2::Header header;

    ByteVector chapterData =
      ByteVector("CHAP"                     // Frame ID
                 "\x00\x00\x00\x20"         // Frame size
                 "\x00\x00"                 // Frame flags
                 "\x43\x00"                 // Element ID ("C")
                 "\x00\x00\x00\x03"         // Start time
                 "\x00\x00\x00\x05"         // End time
                 "\x00\x00\x00\x02"         // Start offset
                 "\x00\x00\x00\x03", 28);   // End offset
    ByteVector embeddedFrameData =
      ByteVector("TIT2"                     // Embedded frame ID
                 "\x00\x00\x00\x04"         // Embedded frame size
                 "\x00\x00"                 // Embedded frame flags
                 "\x00"                     // TIT2 frame text encoding
                 "CH1", 14);                // Chapter title

    ID3v2::ChapterFrame f1(&header, chapterData);

    CPPUNIT_ASSERT_EQUAL(ByteVector("C"), f1.elementID());
    CPPUNIT_ASSERT((unsigned int)0x03 == f1.startTime());
    CPPUNIT_ASSERT((unsigned int)0x05 == f1.endTime());
    CPPUNIT_ASSERT((unsigned int)0x02 == f1.startOffset());
    CPPUNIT_ASSERT((unsigned int)0x03 == f1.endOffset());
    CPPUNIT_ASSERT((unsigned int)0x00 == f1.embeddedFrameList().size());

    ID3v2::ChapterFrame f2(&header, chapterData + embeddedFrameData);

    CPPUNIT_ASSERT_EQUAL(ByteVector("C"), f2.elementID());
    CPPUNIT_ASSERT((unsigned int)0x03 == f2.startTime());
    CPPUNIT_ASSERT((unsigned int)0x05 == f2.endTime());
    CPPUNIT_ASSERT((unsigned int)0x02 == f2.startOffset());
    CPPUNIT_ASSERT((unsigned int)0x03 == f2.endOffset());
    CPPUNIT_ASSERT((unsigned int)0x01 == f2.embeddedFrameList().size());
    CPPUNIT_ASSERT(f2.embeddedFrameList("TIT2").size() == 1);
    CPPUNIT_ASSERT(f2.embeddedFrameList("TIT2")[0]->toString() == "CH1");
  }

  void testRenderChapterFrame()
  {
    ID3v2::Header header;
    ID3v2::ChapterFrame f1(&header, "CHAP");
    f1.setElementID(ByteVector("\x43\x00", 2));
    f1.setStartTime(3);
    f1.setEndTime(5);
    f1.setStartOffset(2);
    f1.setEndOffset(3);
    ID3v2::TextIdentificationFrame *eF = new ID3v2::TextIdentificationFrame("TIT2");
    eF->setText("CH1");
    f1.addEmbeddedFrame(eF);

    ByteVector expected =
      ByteVector("CHAP"                     // Frame ID
                 "\x00\x00\x00\x20"         // Frame size
                 "\x00\x00"                 // Frame flags
                 "\x43\x00"                 // Element ID
                 "\x00\x00\x00\x03"         // Start time
                 "\x00\x00\x00\x05"         // End time
                 "\x00\x00\x00\x02"         // Start offset
                 "\x00\x00\x00\x03"         // End offset
                 "TIT2"                     // Embedded frame ID
                 "\x00\x00\x00\x04"         // Embedded frame size
                 "\x00\x00"                 // Embedded frame flags
                 "\x00"                     // TIT2 frame text encoding
                 "CH1", 42);                // Chapter title

    CPPUNIT_ASSERT_EQUAL(expected, f1.render());

    f1.setElementID("C");

    CPPUNIT_ASSERT_EQUAL(expected, f1.render());

    ID3v2::FrameList frames;
    eF = new ID3v2::TextIdentificationFrame("TIT2");
    eF->setText("CH1");
    frames.append(eF);

    ID3v2::ChapterFrame f2(ByteVector("\x43\x00", 2), 3, 5, 2, 3, frames);
    CPPUNIT_ASSERT_EQUAL(expected, f2.render());

    frames.clear();
    eF = new ID3v2::TextIdentificationFrame("TIT2");
    eF->setText("CH1");
    frames.append(eF);

    ID3v2::ChapterFrame f3(ByteVector("C\x00", 2), 3, 5, 2, 3, frames);
    CPPUNIT_ASSERT_EQUAL(expected, f3.render());

    frames.clear();
    eF = new ID3v2::TextIdentificationFrame("TIT2");
    eF->setText("CH1");
    frames.append(eF);

    ID3v2::ChapterFrame f4("C", 3, 5, 2, 3, frames);
    CPPUNIT_ASSERT_EQUAL(expected, f4.render());

    CPPUNIT_ASSERT(!f4.toString().isEmpty());

    ID3v2::ChapterFrame f5("C", 3, 5, 2, 3);
    eF = new ID3v2::TextIdentificationFrame("TIT2");
    eF->setText("CH1");
    f5.addEmbeddedFrame(eF);
    CPPUNIT_ASSERT_EQUAL(expected, f5.render());
  }

  void testParseTableOfContentsFrame()
  {
    ID3v2::Header header;
    ID3v2::TableOfContentsFrame f(
      &header,
      ByteVector("CTOC"                     // Frame ID
                 "\x00\x00\x00\x16"         // Frame size
                 "\x00\x00"                 // Frame flags
                 "\x54\x00"                 // Element ID ("T")
                 "\x01"                     // CTOC flags
                 "\x02"                     // Entry count
                 "\x43\x00"                 // First entry ("C")
                 "\x44\x00"                 // Second entry ("D")
                 "TIT2"                     // Embedded frame ID
                 "\x00\x00\x00\x04"         // Embedded frame size
                 "\x00\x00"                 // Embedded frame flags
                 "\x00"                     // TIT2 frame text encoding
                 "TC1", 32));               // Table of contents title
    CPPUNIT_ASSERT_EQUAL(ByteVector("T"), f.elementID());
    CPPUNIT_ASSERT(!f.isTopLevel());
    CPPUNIT_ASSERT(f.isOrdered());
    CPPUNIT_ASSERT((unsigned int)0x02 == f.entryCount());
    CPPUNIT_ASSERT_EQUAL(ByteVector("C"), f.childElements()[0]);
    CPPUNIT_ASSERT_EQUAL(ByteVector("D"), f.childElements()[1]);
    CPPUNIT_ASSERT((unsigned int)0x01 == f.embeddedFrameList().size());
    CPPUNIT_ASSERT(f.embeddedFrameList("TIT2").size() == 1);
    CPPUNIT_ASSERT(f.embeddedFrameList("TIT2")[0]->toString() == "TC1");

    f.removeChildElement("E"); // not existing
    CPPUNIT_ASSERT_EQUAL(2U, f.entryCount());
    f.removeChildElement("C");
    CPPUNIT_ASSERT_EQUAL(1U, f.entryCount());
    CPPUNIT_ASSERT_EQUAL(ByteVector("D"), f.childElements()[0]);

    ID3v2::Frame *frame = f.embeddedFrameList("TIT2")[0];
    f.removeEmbeddedFrame(frame);
    CPPUNIT_ASSERT(f.embeddedFrameList("TIT2").isEmpty());
  }

  void testRenderTableOfContentsFrame()
  {
    ID3v2::Header header;
    ID3v2::TableOfContentsFrame f(&header, "CTOC");
    f.setElementID(ByteVector("\x54\x00", 2));
    f.setIsTopLevel(false);
    f.setIsOrdered(true);
    f.addChildElement(ByteVector("\x43\x00", 2));
    f.addChildElement(ByteVector("\x44\x00", 2));
    ID3v2::TextIdentificationFrame *eF = new ID3v2::TextIdentificationFrame("TIT2");
    eF->setText("TC1");
    f.addEmbeddedFrame(eF);
    CPPUNIT_ASSERT_EQUAL(
      ByteVector("CTOC"                     // Frame ID
                 "\x00\x00\x00\x16"         // Frame size
                 "\x00\x00"                 // Frame flags
                 "\x54\x00"                 // Element ID
                 "\x01"                     // CTOC flags
                 "\x02"                     // Entry count
                 "\x43\x00"                 // First entry
                 "\x44\x00"                 // Second entry
                 "TIT2"                     // Embedded frame ID
                 "\x00\x00\x00\x04"         // Embedded frame size
                 "\x00\x00"                 // Embedded frame flags
                 "\x00"                     // TIT2 frame text encoding
                 "TC1", 32),                // Table of contents title
      f.render());
  }

  void testShrinkPadding()
  {
    ScopedFileCopy copy("xing", ".mp3");
    string newname = copy.fileName();

    {
      MPEG::File f(newname.c_str());
      f.ID3v2Tag()->setTitle(longText(64 * 1024));
      f.save(MPEG::File::ID3v2, File::StripOthers);
    }
    {
      MPEG::File f(newname.c_str());
      CPPUNIT_ASSERT(f.hasID3v2Tag());
      CPPUNIT_ASSERT_EQUAL(74789L, f.length());
      f.ID3v2Tag()->setTitle("ABCDEFGHIJ");
      f.save(MPEG::File::ID3v2, File::StripOthers);
    }
    {
      MPEG::File f(newname.c_str());
      CPPUNIT_ASSERT(f.hasID3v2Tag());
      CPPUNIT_ASSERT_EQUAL(9263L, f.length());
    }
  }

  void testEmptyFrame()
  {
    ScopedFileCopy copy("xing", ".mp3");
    string newname = copy.fileName();

    {
      MPEG::File f(newname.c_str());
      ID3v2::Tag *tag = f.ID3v2Tag(true);

      ID3v2::UrlLinkFrame *frame1 = new ID3v2::UrlLinkFrame(
        ByteVector("WOAF\x00\x00\x00\x01\x00\x00\x00", 11));
      tag->addFrame(frame1);

      ID3v2::TextIdentificationFrame *frame2 = new ID3v2::TextIdentificationFrame("TIT2");
      frame2->setText("Title");
      tag->addFrame(frame2);

      f.save();
    }

    {
      MPEG::File f(newname.c_str());
      CPPUNIT_ASSERT_EQUAL(true, f.hasID3v2Tag());

      ID3v2::Tag *tag = f.ID3v2Tag();
      CPPUNIT_ASSERT_EQUAL(String("Title"), tag->title());
      CPPUNIT_ASSERT_EQUAL(true, tag->frameListMap()["WOAF"].isEmpty());
    }
  }

  void testDuplicateTags()
  {
    ScopedFileCopy copy("duplicate_id3v2", ".mp3");

    ByteVector audioStream;
    {
      MPEG::File f(copy.fileName().c_str());
      f.seek(f.ID3v2Tag()->header()->completeTagSize());
      audioStream = f.readBlock(2089);

      // duplicate_id3v2.mp3 has duplicate ID3v2 tags.
      // Sample rate will be 32000 if we can't skip the second tag.

      CPPUNIT_ASSERT(f.hasID3v2Tag());
      CPPUNIT_ASSERT_EQUAL((unsigned int)8049, f.ID3v2Tag()->header()->completeTagSize());

      CPPUNIT_ASSERT_EQUAL(44100, f.audioProperties()->sampleRate());

      f.ID3v2Tag()->setArtist("Artist A");
      f.save(MPEG::File::ID3v2, File::StripOthers);
    }
    {
      MPEG::File f(copy.fileName().c_str());
      CPPUNIT_ASSERT(f.hasID3v2Tag());
      CPPUNIT_ASSERT_EQUAL((long)3594, f.length());
      CPPUNIT_ASSERT_EQUAL((unsigned int)1505, f.ID3v2Tag()->header()->completeTagSize());
      CPPUNIT_ASSERT_EQUAL(String("Artist A"), f.ID3v2Tag()->artist());
      CPPUNIT_ASSERT_EQUAL(44100, f.audioProperties()->sampleRate());

      f.seek(f.ID3v2Tag()->header()->completeTagSize());
      CPPUNIT_ASSERT_EQUAL(f.readBlock(2089), audioStream);

    }
  }

  void testParseTOCFrameWithManyChildren()
  {
    MPEG::File f(TEST_FILE_PATH_C("toc_many_children.mp3"));
    CPPUNIT_ASSERT(f.isValid());

    ID3v2::Tag *tag = f.ID3v2Tag();
    const ID3v2::FrameList &frames = tag->frameList();
    CPPUNIT_ASSERT_EQUAL(130U, frames.size());
    int i = 0;
    for(ID3v2::FrameList::ConstIterator it = frames.begin(); it != frames.end();
        ++it, ++i) {
      if(i > 0) {
        CPPUNIT_ASSERT_EQUAL(ByteVector("CHAP"), (*it)->frameID());
        const ID3v2::ChapterFrame *chapFrame =
            dynamic_cast<const ID3v2::ChapterFrame *>(*it);
        CPPUNIT_ASSERT_EQUAL(ByteVector("chapter") +
                             ByteVector(String::number(i - 1).toCString()),
                             chapFrame->elementID());
        CPPUNIT_ASSERT_EQUAL(static_cast<unsigned int>(100 * i),
                             chapFrame->startTime());
        CPPUNIT_ASSERT_EQUAL(static_cast<unsigned int>(100 * i),
                             chapFrame->endTime());
        const ID3v2::FrameList &embeddedFrames = chapFrame->embeddedFrameList();
        CPPUNIT_ASSERT_EQUAL(1U, embeddedFrames.size());
        const ID3v2::TextIdentificationFrame *tit2Frame =
            dynamic_cast<const ID3v2::TextIdentificationFrame *>(
              embeddedFrames.front());
        CPPUNIT_ASSERT(tit2Frame);
        CPPUNIT_ASSERT_EQUAL(String("Marker ") + String::number(i),
                             tit2Frame->fieldList().front());
      }
      else {
        CPPUNIT_ASSERT_EQUAL(ByteVector("CTOC"), (*it)->frameID());
        const ID3v2::TableOfContentsFrame *ctocFrame =
            dynamic_cast<const ID3v2::TableOfContentsFrame *>(*it);
        CPPUNIT_ASSERT_EQUAL(ByteVector("toc"), ctocFrame->elementID());
        CPPUNIT_ASSERT(!ctocFrame->isTopLevel());
        CPPUNIT_ASSERT(!ctocFrame->isOrdered());
        CPPUNIT_ASSERT_EQUAL(129U, ctocFrame->entryCount());
        const ID3v2::FrameList &embeddedFrames = ctocFrame->embeddedFrameList();
        CPPUNIT_ASSERT_EQUAL(1U, embeddedFrames.size());
        const ID3v2::TextIdentificationFrame *tit2Frame =
            dynamic_cast<const ID3v2::TextIdentificationFrame *>(
              embeddedFrames.front());
        CPPUNIT_ASSERT(tit2Frame);
        CPPUNIT_ASSERT_EQUAL(StringList("toplevel toc"), tit2Frame->fieldList());
      }
    }

    CPPUNIT_ASSERT(!ID3v2::ChapterFrame::findByElementID(tag, "chap2"));
    CPPUNIT_ASSERT(ID3v2::ChapterFrame::findByElementID(tag, "chapter2"));

    CPPUNIT_ASSERT(!ID3v2::TableOfContentsFrame::findTopLevel(tag));
    CPPUNIT_ASSERT(!ID3v2::TableOfContentsFrame::findByElementID(tag, "ctoc"));
    CPPUNIT_ASSERT(ID3v2::TableOfContentsFrame::findByElementID(tag, "toc"));
  }

};

CPPUNIT_TEST_SUITE_REGISTRATION(TestID3v2);

