/*
 * Copyright (C) 2020 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 android.util;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
import static org.xmlpull.v1.XmlPullParser.END_TAG;
import static org.xmlpull.v1.XmlPullParser.START_DOCUMENT;
import static org.xmlpull.v1.XmlPullParser.START_TAG;
import static org.xmlpull.v1.XmlPullParser.TEXT;

import android.os.PersistableBundle;

import androidx.test.ext.junit.runners.AndroidJUnit4;

import com.android.internal.util.XmlUtils;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;

import org.junit.Test;
import org.junit.runner.RunWith;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

@RunWith(AndroidJUnit4.class)
public class XmlTest {
    @Test
    public void testLargeValues_Normal() throws Exception {
        doLargeValues(XmlUtils.makeTyped(Xml.newSerializer()),
                XmlUtils.makeTyped(Xml.newPullParser()));
    }

    @Test
    public void testLargeValues_Fast() throws Exception {
        doLargeValues(Xml.newFastSerializer(),
                Xml.newFastPullParser());
    }

    @Test
    public void testLargeValues_FastIndenting() throws Exception {
        final TypedXmlSerializer out = Xml.newFastSerializer();
        out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
        doLargeValues(out,
                Xml.newFastPullParser());
    }

    @Test
    public void testLargeValues_Binary() throws Exception {
        doLargeValues(Xml.newBinarySerializer(),
                Xml.newBinaryPullParser());
    }

    /**
     * Verify that we can write and read large {@link String} and {@code byte[]}
     * without issues.
     */
    private static void doLargeValues(TypedXmlSerializer out, TypedXmlPullParser in)
            throws Exception {
        final char[] chars = new char[65_534];
        Arrays.fill(chars, '!');

        final String string = new String(chars);
        final byte[] bytes = string.getBytes();
        assertEquals(chars.length, bytes.length);

        final ByteArrayOutputStream os = new ByteArrayOutputStream();
        out.setOutput(os, StandardCharsets.UTF_8.name());
        out.startTag(null, "tag");
        out.attribute(null, "string", string);
        out.attributeBytesBase64(null, "bytes", bytes);
        out.endTag(null, "tag");
        out.flush();

        final ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
        in.setInput(is, StandardCharsets.UTF_8.name());
        assertNext(in, START_TAG, "tag");
        assertEquals(2, in.getAttributeCount());
        assertEquals(string, in.getAttributeValue(null, "string"));
        assertArrayEquals(bytes, in.getAttributeBytesBase64(null, "bytes"));
    }

    @Test
    public void testPersistableBundle_Normal() throws Exception {
        doPersistableBundle(XmlUtils.makeTyped(Xml.newSerializer()),
                XmlUtils.makeTyped(Xml.newPullParser()));
    }

    @Test
    public void testPersistableBundle_Fast() throws Exception {
        doPersistableBundle(Xml.newFastSerializer(),
                Xml.newFastPullParser());
    }

    @Test
    public void testPersistableBundle_FastIndenting() throws Exception {
        final TypedXmlSerializer out = Xml.newFastSerializer();
        out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
        doPersistableBundle(out,
                Xml.newFastPullParser());
    }

    @Test
    public void testPersistableBundle_Binary() throws Exception {
        doPersistableBundle(Xml.newBinarySerializer(),
                Xml.newBinaryPullParser());
    }

    /**
     * Verify that a complex {@link PersistableBundle} can be serialized out and
     * then parsed in with the original structure intact.
     */
    private static void doPersistableBundle(TypedXmlSerializer out, TypedXmlPullParser in)
            throws Exception {
        final PersistableBundle expected = buildPersistableBundle();
        final byte[] raw = doPersistableBundleWrite(out, expected);

        // Yes, this string-based check is fragile, but kindofEquals() is broken
        // when working with nested objects and arrays
        final PersistableBundle actual = doPersistableBundleRead(in, raw);
        assertEquals(expected.toString(), actual.toString());
    }

    static PersistableBundle buildPersistableBundle() {
        final PersistableBundle outer = new PersistableBundle();

        outer.putBoolean("boolean", true);
        outer.putInt("int", 42);
        outer.putLong("long", 43L);
        outer.putDouble("double", 44d);
        outer.putString("string", "com.example <and></and> &amp; more");

        outer.putBooleanArray("boolean[]", new boolean[] { true, false, true });
        outer.putIntArray("int[]", new int[] { 42, 43, 44 });
        outer.putLongArray("long[]", new long[] { 43L, 44L, 45L });
        outer.putDoubleArray("double[]", new double[] { 43d, 44d, 45d });
        outer.putStringArray("string[]", new String[] { "foo", "bar", "baz" });

        outer.putString("nullString", null);
        outer.putObject("nullObject", null);
        outer.putIntArray("nullArray", null);

        final PersistableBundle nested = new PersistableBundle();
        nested.putString("nested_key", "nested_value");
        outer.putPersistableBundle("nested", nested);

        return outer;
    }

    static byte[] doPersistableBundleWrite(TypedXmlSerializer out, PersistableBundle bundle)
            throws Exception {
        // We purposefully omit START/END_DOCUMENT events here to verify correct
        // behavior of what PersistableBundle does internally
        final ByteArrayOutputStream os = new ByteArrayOutputStream();
        out.setOutput(os, StandardCharsets.UTF_8.name());
        out.startTag(null, "bundle");
        bundle.saveToXml(out);
        out.endTag(null, "bundle");
        out.flush();
        return os.toByteArray();
    }

    static PersistableBundle doPersistableBundleRead(TypedXmlPullParser in, byte[] raw)
            throws Exception {
        final ByteArrayInputStream is = new ByteArrayInputStream(raw);
        in.setInput(is, StandardCharsets.UTF_8.name());
        in.next();
        return PersistableBundle.restoreFromXml(in);
    }

    @Test
    public void testVerify_Normal() throws Exception {
        doVerify(XmlUtils.makeTyped(Xml.newSerializer()),
                XmlUtils.makeTyped(Xml.newPullParser()));
    }

    @Test
    public void testVerify_Fast() throws Exception {
        doVerify(Xml.newFastSerializer(),
                Xml.newFastPullParser());
    }

    @Test
    public void testVerify_FastIndenting() throws Exception {
        final TypedXmlSerializer out = Xml.newFastSerializer();
        out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
        doVerify(out,
                Xml.newFastPullParser());
    }

    @Test
    public void testVerify_Binary() throws Exception {
        doVerify(Xml.newBinarySerializer(),
                Xml.newBinaryPullParser());
    }

    /**
     * Verify that example test data is correctly serialized and parsed
     * end-to-end using the given objects.
     */
    private static void doVerify(TypedXmlSerializer out, TypedXmlPullParser in) throws Exception {
        final ByteArrayOutputStream os = new ByteArrayOutputStream();
        out.setOutput(os, StandardCharsets.UTF_8.name());
        doVerifyWrite(out);
        out.flush();

        final ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
        in.setInput(is, StandardCharsets.UTF_8.name());
        doVerifyRead(in);
    }

    private static final String TEST_STRING = "com☃example😀typical☃package😀name";
    private static final String TEST_STRING_EMPTY = "";
    private static final byte[] TEST_BYTES = new byte[] { 0, 1, 2, 3, 4, 3, 2, 1, 0 };
    private static final byte[] TEST_BYTES_EMPTY = new byte[0];

    static void doVerifyWrite(TypedXmlSerializer out) throws Exception {
        out.startDocument(StandardCharsets.UTF_8.name(), true);
        out.startTag(null, "one");
        {
            out.startTag(null, "two");
            {
                out.attribute(null, "string", TEST_STRING);
                out.attribute(null, "stringEmpty", TEST_STRING_EMPTY);
                out.attributeBytesHex(null, "bytesHex", TEST_BYTES);
                out.attributeBytesHex(null, "bytesHexEmpty", TEST_BYTES_EMPTY);
                out.attributeBytesBase64(null, "bytesBase64", TEST_BYTES);
                out.attributeBytesBase64(null, "bytesBase64Empty", TEST_BYTES_EMPTY);
                out.attributeInt(null, "int", 43);
                out.attributeIntHex(null, "intHex", 44);
                out.attributeLong(null, "long", 45L);
                out.attributeLongHex(null, "longHex", 46L);
                out.attributeFloat(null, "float", 47f);
                out.attributeDouble(null, "double", 48d);
                out.attributeBoolean(null, "boolean", true);
                out.attribute(null, "stringNumber", "49");
            }
            out.endTag(null, "two");

            out.startTag(null, "three");
            {
                out.text("foo");
                out.startTag(null, "four");
                {
                }
                out.endTag(null, "four");
                out.text("bar");
                out.text("baz");
            }
            out.endTag(null, "three");
        }
        out.endTag(null, "one");
        out.endDocument();
    }

    static void doVerifyRead(TypedXmlPullParser in) throws Exception {
        assertEquals(START_DOCUMENT, in.getEventType());
        assertDepth(in, 0);
        assertNext(in, START_TAG, "one");
        assertDepth(in, 1);
        {
            assertNext(in, START_TAG, "two");
            assertDepth(in, 2);
            {
                assertEquals(14, in.getAttributeCount());
                assertEquals(TEST_STRING,
                        in.getAttributeValue(null, "string"));
                assertEquals(TEST_STRING_EMPTY,
                        in.getAttributeValue(null, "stringEmpty"));
                assertArrayEquals(TEST_BYTES,
                        in.getAttributeBytesHex(null, "bytesHex"));
                assertArrayEquals(TEST_BYTES_EMPTY,
                        in.getAttributeBytesHex(null, "bytesHexEmpty"));
                assertArrayEquals(TEST_BYTES,
                        in.getAttributeBytesBase64(null, "bytesBase64"));
                assertArrayEquals(TEST_BYTES_EMPTY,
                        in.getAttributeBytesBase64(null, "bytesBase64Empty"));

                assertEquals(43, in.getAttributeInt(null, "int"));
                assertEquals(44, in.getAttributeIntHex(null, "intHex"));
                assertEquals(45L, in.getAttributeLong(null, "long"));
                assertEquals(46L, in.getAttributeLongHex(null, "longHex"));
                assertEquals(47f, in.getAttributeFloat(null, "float"), 0.01);
                assertEquals(48d, in.getAttributeDouble(null, "double"), 0.01);
                assertEquals(true, in.getAttributeBoolean(null, "boolean"));

                // Also verify that typed values are available as strings
                assertEquals("000102030403020100", in.getAttributeValue(null, "bytesHex"));
                assertEquals("AAECAwQDAgEA", in.getAttributeValue(null, "bytesBase64"));
                assertEquals("43", in.getAttributeValue(null, "int"));
                assertEquals("2c", in.getAttributeValue(null, "intHex"));
                assertEquals("45", in.getAttributeValue(null, "long"));
                assertEquals("2e", in.getAttributeValue(null, "longHex"));
                assertEquals("true", in.getAttributeValue(null, "boolean"));

                // And that raw strings can be parsed too
                assertEquals("49", in.getAttributeValue(null, "stringNumber"));
                assertEquals(49, in.getAttributeInt(null, "stringNumber"));
            }
            assertNext(in, END_TAG, "two");
            assertDepth(in, 2);

            assertNext(in, START_TAG, "three");
            assertDepth(in, 2);
            {
                assertNext(in, TEXT, null);
                assertDepth(in, 2);
                assertEquals("foo", in.getText().trim());
                assertNext(in, START_TAG, "four");
                assertDepth(in, 3);
                {
                    assertEquals(0, in.getAttributeCount());
                }
                assertNext(in, END_TAG, "four");
                assertDepth(in, 3);
                assertNext(in, TEXT, null);
                assertDepth(in, 2);
                assertEquals("barbaz", in.getText().trim());
            }
            assertNext(in, END_TAG, "three");
            assertDepth(in, 2);
        }
        assertNext(in, END_TAG, "one");
        assertDepth(in, 1);
        assertNext(in, END_DOCUMENT, null);
        assertDepth(in, 0);
    }

    static void assertNext(TypedXmlPullParser in, int token, String name) throws Exception {
        // We're willing to skip over empty text regions, which some
        // serializers emit transparently
        int event;
        while ((event = in.next()) == TEXT && in.getText().trim().length() == 0) {
        }
        assertEquals("next", token, event);
        assertEquals("getEventType", token, in.getEventType());
        assertEquals("getName", name, in.getName());
    }

    static void assertDepth(TypedXmlPullParser in, int depth) throws Exception {
        assertEquals("getDepth", depth, in.getDepth());
    }
}
