/*
 * Copyright (C) 2008 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.core;

import junit.framework.Assert;
import junit.framework.TestCase;

import java.io.File;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.util.Arrays;
import java.util.ConcurrentModificationException;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Random;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Logger;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import android.test.suitebuilder.annotation.MediumTest;
import android.test.suitebuilder.annotation.SmallTest;
import android.test.suitebuilder.annotation.LargeTest;

public class MiscRegressionTest extends TestCase {

    // Regression test for #857840: want JKS key store
    @SmallTest
    public void testDefaultKeystore() {
        String type = KeyStore.getDefaultType();
        Assert.assertEquals("Default keystore type must be Bouncy Castle", "BKS", type);

        try {
            KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType());
            Assert.assertNotNull("Keystore must not be null", store);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
        
        try {
            KeyStore store = KeyStore.getInstance("BKS");
            Assert.assertNotNull("Keystore must not be null", store);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    // Regression test for #1061945: negative Shorts do not
    // serialize/deserialize correctly
    @SmallTest
    public void testShortSerialization() throws Exception {
        // create an instance of ObjectInputStream
        String x = new String("serialize_foobar");
        java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
        (new java.io.ObjectOutputStream(baos)).writeObject(x);
        ObjectInputStream ois = new java.io.ObjectInputStream(
                new java.io.ByteArrayInputStream(baos.toByteArray()));

        // get the setField(...,, short val) method in question
        Class<ObjectInputStream> oClass = ObjectInputStream.class;
        Method m = oClass.getDeclaredMethod("setField", new Class[] { Object.class, Class.class, String.class, short.class});
        // compose args
        short start = 123;
        short origval = -1; // 0xffff
        Short obj = new Short(start);
        Class<Short> declaringClass = Short.class;
        String fieldDescName = "value";

        // test the initial value
        assertEquals(obj.shortValue(), start);
        // invoke native method to set the field "value" of type short to the newval
        m.setAccessible(true); // since the method is private
        m.invoke(ois, new Object[]{ obj, declaringClass, fieldDescName, new Short(origval)} );
        // test the set value
        short res = obj.shortValue();
        assertEquals("Read and written values must be equal", origval, res);
    }
    
    // Regression test for #951285: Suitable LogHandler should be chosen
    // depending on the environment.
    @MediumTest
    public void testAndroidLogHandler() throws Exception {
        Logger.global.severe("This has logging Level.SEVERE, should become ERROR");
        Logger.global.warning("This has logging Level.WARNING, should become WARN");
        Logger.global.info("This has logging Level.INFO, should become INFO");
        Logger.global.config("This has logging Level.CONFIG, should become DEBUG");
        Logger.global.fine("This has logging Level.FINE, should become VERBOSE");
        Logger.global.finer("This has logging Level.FINER, should become VERBOSE");
        Logger.global.finest("This has logging Level.FINEST, should become VERBOSE");
    }

    // Regression test for #1045939: Different output for Method.toString()
    @SmallTest
    public void testMethodToString() {
        try {
            Method m1 = Object.class.getMethod("notify", new Class[] { });
            Method m2 = Object.class.getMethod("toString", new Class[] { });
            Method m3 = Object.class.getMethod("wait", new Class[] { long.class, int.class });
            Method m4 = Object.class.getMethod("equals", new Class[] { Object.class });
            Method m5 = String.class.getMethod("valueOf", new Class[] { char[].class });
            Method m6 = Runtime.class.getMethod("exec", new Class[] { String[].class });

            assertEquals("Method.toString() must match expectations",
                    "public final native void java.lang.Object.notify()",
                    m1.toString());
            
            assertEquals("Method.toString() must match expectations",
                    "public java.lang.String java.lang.Object.toString()",
                    m2.toString());

            assertEquals("Method.toString() must match expectations",
                    "public final native void java.lang.Object.wait(long,int) throws java.lang.InterruptedException",
                    m3.toString());

            assertEquals("Method.toString() must match expectations",
                    "public boolean java.lang.Object.equals(java.lang.Object)",
                    m4.toString());

            assertEquals("Method.toString() must match expectations",
                    "public static java.lang.String java.lang.String.valueOf(char[])",
                    m5.toString());

            assertEquals("Method.toString() must match expectations",
                    "public java.lang.Process java.lang.Runtime.exec(java.lang.String[]) throws java.io.IOException",
                    m6.toString());

        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }

    }

    // Regression test for #1062200: Enum fails to deserialize. Actual problem
    // was that Class.isEnum() erroneously returned true for indirect
    // descendants of Enum.
    enum TrafficLights {
        RED,
        YELLOW {},
        GREEN {
            @SuppressWarnings("unused")
            int i;
            @SuppressWarnings("unused")
            void foobar() {}
        };
    }
    
    @SmallTest
    public void testClassIsEnum() {
        Class<?> trafficClass = TrafficLights.class;
        
        Class<?> redClass = TrafficLights.RED.getClass();
        Class<?> yellowClass = TrafficLights.YELLOW.getClass();
        Class<?> greenClass = TrafficLights.GREEN.getClass();
        
        Assert.assertSame("Classes must be equal", trafficClass, redClass);
        Assert.assertNotSame("Classes must be different", trafficClass, yellowClass);
        Assert.assertNotSame("Classes must be different", trafficClass, greenClass);
        Assert.assertNotSame("Classes must be different", yellowClass, greenClass);
        
        Assert.assertTrue("Must be an enum", trafficClass.isEnum());
        Assert.assertTrue("Must be an enum", redClass.isEnum());
        Assert.assertFalse("Must not be an enum", yellowClass.isEnum());
        Assert.assertFalse("Must not be an enum", greenClass.isEnum());
        
        Assert.assertNotNull("Must have enum constants", trafficClass.getEnumConstants());
        Assert.assertNull("Must not have enum constants", yellowClass.getEnumConstants());
        Assert.assertNull("Must not have enum constants", greenClass.getEnumConstants());
    }
    
    // Regression test for #1046174: JarEntry.getCertificates() is really slow.
    public void checkJarCertificates(File file) {
        try {
            JarFile jarFile = new JarFile(file);
            JarEntry je = jarFile.getJarEntry("AndroidManifest.xml");
            byte[] readBuffer = new byte[1024];
            
            long t0 = System.currentTimeMillis();
            
            // We must read the stream for the JarEntry to retrieve
            // its certificates.
            InputStream is = jarFile.getInputStream(je);
            while (is.read(readBuffer, 0, readBuffer.length) != -1) {
                // not using
            }
            is.close();
            Certificate[] certs = je != null ? je.getCertificates() : null;

            long t1 = System.currentTimeMillis();
            android.util.Log.d("TestHarness", "loadCertificates() took " + (t1 - t0) + " ms");
            if (certs == null) {
                android.util.Log.d("TestHarness", "We have no certificates");
            } else {
                android.util.Log.d("TestHarness", "We have " + certs.length + " certificates");
            }
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }
    
    @LargeTest
    public void testJarCertificates() {
        File[] files = new File("/system/app").listFiles();
        for (int i = 0; i < files.length; i++) {
            checkJarCertificates(files[i]);
        }
    }
    
    // Regression test for #1120750: Reflection for static long fields is broken
    private static final long MY_LONG = 5073258162644648461L;
    
    @SmallTest
    public void testLongFieldReflection() {
        try {
            Field field = getClass().getDeclaredField("MY_LONG");
            assertEquals(5073258162644648461L, field.getLong(null));
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    // Regression test for Harmony LinkedHashMap bug. Copied from core, just
    // to make sure it doesn't get lost.
    @SmallTest
    public void testLinkedHashMap() {
        // we want to test the LinkedHashMap in access ordering mode.
        LinkedHashMap map = new LinkedHashMap<String, String>(10, 0.75f, true);

        map.put("key1", "value1");
        map.put("key2", "value2");
        map.put("key3", "value3");

        Iterator iterator = map.keySet().iterator();
        String id = (String) iterator.next();
        map.get(id);
        try {
            iterator.next();
            // A LinkedHashMap is supposed to throw this Exception when a 
            // iterator.next() Operation takes place after a get
            // Operation. This is because the get Operation is considered
            // a structural modification if the LinkedHashMap is in
            // access order mode.
            fail("expected ConcurrentModificationException was not thrown.");
        } catch(ConcurrentModificationException e) {
            // expected
        }
        
        LinkedHashMap mapClone = (LinkedHashMap) map.clone();
        
        iterator = map.keySet().iterator();
        id = (String) iterator.next();
        mapClone.get(id);
        try {
            iterator.next();
        } catch(ConcurrentModificationException e) {
            fail("expected ConcurrentModificationException was not thrown.");
        }
    }
    
    // Regression test for #1212257: Boot-time package scan is slow. Not
    // expected to fail. Please see log if you are interested in the results.
    @LargeTest
    public void testZipStressManifest() {
        android.util.Log.d("MiscRegressionTest", "ZIP stress test started");
        
        long time0 = System.currentTimeMillis();
        
        try {
            File[] files = new File("/system/app").listFiles();

            byte[] buffer = new byte[512];
            
            if (files != null) {
                for (int i = 0; i < files.length; i++) {
                    android.util.Log.d("MiscRegressionTest",
                            "ZIP stress test processing " + files[i] + "...");
                    
                    ZipFile zip = new ZipFile(files[i]);
                    
                    ZipEntry entry = zip.getEntry("AndroidManifest.xml");
                    InputStream stream = zip.getInputStream(entry);
                    
                    int j = stream.read(buffer);
                    while (j != -1) {
                        j = stream.read(buffer);
                    }
                    
                    stream.close();
                }
            }
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
        
        long time1 = System.currentTimeMillis();
        
        android.util.Log.d("MiscRegressionTest", "ZIP stress test finished, " +
                "time was " + (time1- time0) + "ms");
    }

    @LargeTest
    public void testZipStressAllFiles() {
        android.util.Log.d("MiscRegressionTest", "ZIP stress test started");
        
        long time0 = System.currentTimeMillis();
        
        try {
            File[] files = new File("/system/app").listFiles();

            byte[] buffer = new byte[512];
            
            if (files != null) {
                for (int i = 0; i < files.length; i++) {
                    android.util.Log.d("MiscRegressionTest",
                            "ZIP stress test processing " + files[i] + "...");
                    
                    ZipFile zip = new ZipFile(files[i]);
                    
                    Enumeration<? extends ZipEntry> entries = zip.entries();
                    while (entries.hasMoreElements()) {
                        InputStream stream = zip.getInputStream(entries.nextElement());
                        
                        int j = stream.read(buffer);
                        while (j != -1) {
                            j = stream.read(buffer);
                        }
                        
                        stream.close();
                    }
                }
            }
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
        
        long time1 = System.currentTimeMillis();
        
        android.util.Log.d("MiscRegressionTest", "ZIP stress test finished, " +
                "time was " + (time1- time0) + "ms");
    }

    @SmallTest
    public void testOsEncodingProperty() {
        long time0 = System.currentTimeMillis();
        String[] files = new File("/system/app").list();
        long time1 = System.currentTimeMillis();
        android.util.Log.d("MiscRegressionTest", "File.list() test finished, " +
                "time was " + (time1- time0) + "ms");
    }

    // -------------------------------------------------------------------------
    // Regression test for #1185084: Native memory allocated by
    // java.util.zip.Deflater in system_server. The fix reduced some internal
    // ZLIB buffers in size, so this test is trying to execute a lot of
    // deflating to ensure that things are still working properly.
    private void assertEquals(byte[] a, byte[] b) {
        assertEquals("Arrays must have same length", a.length, b.length);
        
        for (int i = 0; i < a.length; i++) {
            assertEquals("Array elements #" + i + " must be equal", a[i], b[i]);
        }
    }
    
    @LargeTest
    public void testZipDeflateInflateStress() {
        
        final int DATA_SIZE = 16384;

        Random random = new Random(42); // Seed makes test reproducible
        
        try {
            // Outer loop selects "mode" of test.
            for (int j = 1; j <=2 ; j++) {

                byte[] input = new byte[DATA_SIZE];
                
                if (j == 1) {
                    // Totally random content
                    random.nextBytes(input);
                } else {
                    // Random contents with longer repetitions
                    int pos = 0;
                    while (pos < input.length) {
                        byte what = (byte)random.nextInt(256);
                        int howMany = random.nextInt(32);
                        if (pos + howMany >= input.length) {
                            howMany = input.length - pos;
                        }
                        Arrays.fill(input, pos, pos + howMany, what);
                        pos += howMany;
                    }
                }
                
                // Inner loop tries all 9 compression levels.
                for (int i = 1; i <= 9; i++) {
                    android.util.Log.d("MiscRegressionTest", "ZipDeflateInflateStress test (" + j + "," + i + ")...");
                    
                    byte[] zipped = new byte[2 * DATA_SIZE]; // Just to make sure...
                    
                    Deflater deflater = new Deflater(i);
                    deflater.setInput(input);
                    deflater.finish();
                    
                    deflater.deflate(zipped);
                    
                    byte[] output = new byte[DATA_SIZE];
                    
                    Inflater inflater = new Inflater();
                    inflater.setInput(zipped);
                    inflater.finished();
    
                    inflater.inflate(output);
                    
                    assertEquals(input, output);
                }
            }
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }
    
    // -------------------------------------------------------------------------
    // Regression test for #1252043: Thread.getStackTrace() is broken
    class MyThread extends Thread {
        public MyThread(String name) {
            super(name);
        }
        
        @Override
        public void run() {
            doSomething();
        }
        
        public void doSomething() {
            for (int i = 0; i < 20;) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException ex) {
                }
            }
        }
    }

    class MyOtherThread extends Thread {
        public int visibleTraces;
        
        public MyOtherThread(ThreadGroup group, String name) {
            super(group, name);
        }
        
        @Override
        public void run() {
            visibleTraces = Thread.getAllStackTraces().size();
        }
    }
    
    @LargeTest
    public void testThreadGetStackTrace() {
        MyThread t1 = new MyThread("t1");
        t1.start();
        
        try {
            Thread.sleep(1000);
        } catch (InterruptedException ex) {
        }

        StackTraceElement[] traces = t1.getStackTrace();
        StackTraceElement trace = traces[traces.length - 2];
        
        // Expect to find MyThread.doSomething in the trace
        assertTrue("Must find MyThread.doSomething in trace",
                trace.getClassName().endsWith("$MyThread") && 
                trace.getMethodName().equals("doSomething"));

        ThreadGroup g1 = new ThreadGroup("1");
        MyOtherThread t2 = new MyOtherThread(g1, "t2");
        t2.start();
        try {
            t2.join();
        } catch (InterruptedException ex) {
        }
        
        // Expect to see the traces of all threads (not just t2)
        assertTrue("Must have traces for all threads", t2.visibleTraces > 1);
    }
}
