/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.mediaframeworktest.unit;
import android.media.Metadata;
import android.os.Parcel;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
import android.util.Log;

import java.util.Calendar;
import java.util.Date;

/*
 * Check the Java layer that parses serialized metadata in Parcel
 * works as expected.
 *
 */

public class MediaPlayerMetadataParserTest extends AndroidTestCase {
    private static final String TAG = "MediaPlayerMetadataTest";
    private static final int kMarker = 0x4d455441;  // 'M' 'E' 'T' 'A'
    private static final int kHeaderSize = 8;

    private Metadata mMetadata = null;
    private Parcel mParcel = null;

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        mMetadata = new Metadata();
        mParcel = Parcel.obtain();

        resetParcel();
    }

    // Check parsing of the parcel fails. Make sure the parser rewind
    // the parcel properly.
    private void assertParseFail() throws Exception {
        mParcel.setDataPosition(0);
        assertFalse(mMetadata.parse(mParcel));
        assertEquals(0, mParcel.dataPosition());
    }

    // Check parsing of the parcel is successful.
    private void assertParse() throws Exception {
        mParcel.setDataPosition(0);
        assertTrue(mMetadata.parse(mParcel));
    }

    // Write the number of bytes from the start of the parcel to the
    // current position at the beginning of the parcel (offset 0).
    private void adjustSize() {
        adjustSize(0);
    }

    // Write the number of bytes from the offset to the current
    // position at position pointed by offset.
    private void adjustSize(int offset) {
        final int pos = mParcel.dataPosition();

        mParcel.setDataPosition(offset);
        mParcel.writeInt(pos - offset);
        mParcel.setDataPosition(pos);
    }

    // Rewind the parcel and insert the header.
    private void resetParcel() {
        mParcel.setDataPosition(0);
        // Most tests will use a properly formed parcel with a size
        // and the meta marker so we add them by default.
        mParcel.writeInt(-1);  // Placeholder for the size
        mParcel.writeInt(kMarker);
    }

    // ----------------------------------------------------------------------
    // START OF THE TESTS


    // There should be at least 8 bytes in the parcel, 4 for the size
    // and 4 for the 'M' 'E' 'T' 'A' marker.
    @SmallTest
    public void testMissingSizeAndMarker() throws Exception {
        for (int i = 0; i < kHeaderSize; ++i) {
            mParcel.setDataPosition(0);
            mParcel.setDataSize(i);

            assertEquals(i, mParcel.dataAvail());
            assertParseFail();
        }
    }

    // There should be at least 'size' bytes in the parcel.
    @SmallTest
    public void testMissingData() throws Exception {
        final int size = 20;

        mParcel.writeInt(size);
        mParcel.setDataSize(size - 1);
        assertParseFail();
    }

    // Empty parcel is fine
    @SmallTest
    public void testEmptyIsOk() throws Exception {
        adjustSize();
        assertParse();
    }

    // ----------------------------------------------------------------------
    // RECORDS
    // ----------------------------------------------------------------------

    // A record header should be at least 12 bytes long
    @SmallTest
    public void testRecordMissingId() throws Exception {
        mParcel.writeInt(13); // record length
        // misses metadata id and metadata type.
        adjustSize();
        assertParseFail();
    }

    @SmallTest
    public void testRecordMissingType() throws Exception {
        mParcel.writeInt(13); // record length lies
        mParcel.writeInt(Metadata.TITLE);
        // misses metadata type
        adjustSize();
        assertParseFail();
    }

    @SmallTest
    public void testRecordWithZeroPayload() throws Exception {
        mParcel.writeInt(0);
        adjustSize();
        assertParseFail();
    }

    // A record cannot be empty.
    @SmallTest
    public void testRecordMissingPayload() throws Exception {
        mParcel.writeInt(12);
        mParcel.writeInt(Metadata.TITLE);
        mParcel.writeInt(Metadata.STRING_VAL);
        // misses payload
        adjustSize();
        assertParseFail();
    }

    // Check records can be found.
    @SmallTest
    public void testRecordsFound() throws Exception {
        writeStringRecord(Metadata.TITLE, "a title");
        writeStringRecord(Metadata.GENRE, "comedy");
        writeStringRecord(Metadata.firstCustomId(), "custom");
        adjustSize();
        assertParse();
        assertTrue(mMetadata.has(Metadata.TITLE));
        assertTrue(mMetadata.has(Metadata.GENRE));
        assertTrue(mMetadata.has(Metadata.firstCustomId()));
        assertFalse(mMetadata.has(Metadata.DRM_CRIPPLED));
        assertEquals(3, mMetadata.keySet().size());
    }

    // Detects bad metadata type
    @SmallTest
    public void testBadMetadataType() throws Exception {
        final int start = mParcel.dataPosition();
        mParcel.writeInt(-1);  // Placeholder for the length
        mParcel.writeInt(Metadata.TITLE);
        mParcel.writeInt(0);  // Invalid type.
        mParcel.writeString("dummy");
        adjustSize(start);

        adjustSize();
        assertParseFail();
    }

    // Check a Metadata instance can be reused, i.e the parse method
    // wipes out the existing states/keys.
    @SmallTest
    public void testParseClearState() throws Exception {
        writeStringRecord(Metadata.TITLE, "a title");
        writeStringRecord(Metadata.GENRE, "comedy");
        writeStringRecord(Metadata.firstCustomId(), "custom");
        adjustSize();
        assertParse();

        resetParcel();
        writeStringRecord(Metadata.MIME_TYPE, "audio/mpg");
        adjustSize();
        assertParse();

        // Only the mime type metadata should be present.
        assertEquals(1, mMetadata.keySet().size());
        assertTrue(mMetadata.has(Metadata.MIME_TYPE));

        assertFalse(mMetadata.has(Metadata.TITLE));
        assertFalse(mMetadata.has(Metadata.GENRE));
        assertFalse(mMetadata.has(Metadata.firstCustomId()));
    }

    // ----------------------------------------------------------------------
    // GETTERS
    // ----------------------------------------------------------------------

    // getString
    @SmallTest
    public void testGetString() throws Exception {
        writeStringRecord(Metadata.TITLE, "a title");
        writeStringRecord(Metadata.GENRE, "comedy");
        adjustSize();
        assertParse();

        assertEquals("a title", mMetadata.getString(Metadata.TITLE));
        assertEquals("comedy", mMetadata.getString(Metadata.GENRE));
    }

    // get an empty string.
    @SmallTest
    public void testGetEmptyString() throws Exception {
        writeStringRecord(Metadata.TITLE, "");
        adjustSize();
        assertParse();

        assertEquals("", mMetadata.getString(Metadata.TITLE));
    }

    // get a string when a NULL value was in the parcel
    @SmallTest
    public void testGetNullString() throws Exception {
        writeStringRecord(Metadata.TITLE, null);
        adjustSize();
        assertParse();

        assertEquals(null, mMetadata.getString(Metadata.TITLE));
    }

    // get a string when an integer is actually present
    @SmallTest
    public void testWrongType() throws Exception {
        writeIntRecord(Metadata.DURATION, 5);
        adjustSize();
        assertParse();

        try {
            mMetadata.getString(Metadata.DURATION);
        } catch (IllegalStateException ise) {
            return;
        }
        fail("Exception was not thrown");
    }

    // getInt
    @SmallTest
    public void testGetInt() throws Exception {
        writeIntRecord(Metadata.CD_TRACK_NUM, 1);
        adjustSize();
        assertParse();

        assertEquals(1, mMetadata.getInt(Metadata.CD_TRACK_NUM));
    }

    // getBoolean
    @SmallTest
    public void testGetBoolean() throws Exception {
        writeBooleanRecord(Metadata.DRM_CRIPPLED, true);
        adjustSize();
        assertParse();

        assertEquals(true, mMetadata.getBoolean(Metadata.DRM_CRIPPLED));
    }

    // getLong
    @SmallTest
    public void testGetLong() throws Exception {
        writeLongRecord(Metadata.DURATION, 1L);
        adjustSize();
        assertParse();

        assertEquals(1L, mMetadata.getLong(Metadata.DURATION));
    }

    // getDouble
    @SmallTest
    public void testGetDouble() throws Exception {
        writeDoubleRecord(Metadata.VIDEO_FRAME_RATE, 29.97);
        adjustSize();
        assertParse();

        assertEquals(29.97, mMetadata.getDouble(Metadata.VIDEO_FRAME_RATE));
    }

    // getByteArray
    @SmallTest
    public void testGetByteArray() throws Exception {
        byte data[] = new byte[]{1,2,3,4,5};

        writeByteArrayRecord(Metadata.ALBUM_ART, data);
        adjustSize();
        assertParse();

        byte res[] = mMetadata.getByteArray(Metadata.ALBUM_ART);
        for (int i = 0; i < data.length; ++i) {
            assertEquals(data[i], res[i]);
        }
    }

    // getDate
    @SmallTest
    public void testGetDate() throws Exception {
        writeDateRecord(Metadata.DATE, 0, "PST");
        adjustSize();
        assertParse();

        assertEquals(new Date(0), mMetadata.getDate(Metadata.DATE));
    }

    // getTimedText
    @SmallTest
    public void testGetTimedText() throws Exception {
        Date now = Calendar.getInstance().getTime();
        writeTimedTextRecord(Metadata.CAPTION, now.getTime(),
                             10, "Some caption");
        adjustSize();
        assertParse();

        Metadata.TimedText caption = mMetadata.getTimedText(Metadata.CAPTION);
        assertEquals("" + now + "-" + 10 + ":Some caption", caption.toString());
    }

    // ----------------------------------------------------------------------
    // HELPERS TO APPEND RECORDS
    // ----------------------------------------------------------------------

    // Insert a string record at the current position.
    private void writeStringRecord(int metadataId, String val) {
        final int start = mParcel.dataPosition();
        mParcel.writeInt(-1);  // Placeholder for the length
        mParcel.writeInt(metadataId);
        mParcel.writeInt(Metadata.STRING_VAL);
        mParcel.writeString(val);
        adjustSize(start);
    }

    // Insert an int record at the current position.
    private void writeIntRecord(int metadataId, int val) {
        final int start = mParcel.dataPosition();
        mParcel.writeInt(-1);  // Placeholder for the length
        mParcel.writeInt(metadataId);
        mParcel.writeInt(Metadata.INTEGER_VAL);
        mParcel.writeInt(val);
        adjustSize(start);
    }

    // Insert a boolean record at the current position.
    private void writeBooleanRecord(int metadataId, boolean val) {
        final int start = mParcel.dataPosition();
        mParcel.writeInt(-1);  // Placeholder for the length
        mParcel.writeInt(metadataId);
        mParcel.writeInt(Metadata.BOOLEAN_VAL);
        mParcel.writeInt(val ? 1 : 0);
        adjustSize(start);
    }

    // Insert a Long record at the current position.
    private void writeLongRecord(int metadataId, long val) {
        final int start = mParcel.dataPosition();
        mParcel.writeInt(-1);  // Placeholder for the length
        mParcel.writeInt(metadataId);
        mParcel.writeInt(Metadata.LONG_VAL);
        mParcel.writeLong(val);
        adjustSize(start);
    }

    // Insert a Double record at the current position.
    private void writeDoubleRecord(int metadataId, double val) {
        final int start = mParcel.dataPosition();
        mParcel.writeInt(-1);  // Placeholder for the length
        mParcel.writeInt(metadataId);
        mParcel.writeInt(Metadata.DOUBLE_VAL);
        mParcel.writeDouble(val);
        adjustSize(start);
    }

    // Insert a ByteArray record at the current position.
    private void writeByteArrayRecord(int metadataId, byte[] val) {
        final int start = mParcel.dataPosition();
        mParcel.writeInt(-1);  // Placeholder for the length
        mParcel.writeInt(metadataId);
        mParcel.writeInt(Metadata.BYTE_ARRAY_VAL);
        mParcel.writeByteArray(val);
        adjustSize(start);
    }

    // Insert a Date record at the current position.
    private void writeDateRecord(int metadataId, long time, String tz) {
        final int start = mParcel.dataPosition();
        mParcel.writeInt(-1);  // Placeholder for the length
        mParcel.writeInt(metadataId);
        mParcel.writeInt(Metadata.DATE_VAL);
        mParcel.writeLong(time);
        mParcel.writeString(tz);
        adjustSize(start);
    }

    // Insert a TimedText record at the current position.
    private void writeTimedTextRecord(int metadataId, long begin,
                                      int duration, String text) {
        final int start = mParcel.dataPosition();
        mParcel.writeInt(-1);  // Placeholder for the length
        mParcel.writeInt(metadataId);
        mParcel.writeInt(Metadata.TIMED_TEXT_VAL);
        mParcel.writeLong(begin);
        mParcel.writeInt(duration);
        mParcel.writeString(text);
        adjustSize(start);
    }
}
