/*
 * Copyright (C) 2016 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.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import android.os.Parcel;
import android.support.test.runner.AndroidJUnit4;
import libcore.io.IoUtils;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.lang.reflect.Field;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

@RunWith(AndroidJUnit4.class)
public class MemoryIntArrayTest {
    static {
        System.loadLibrary("cutils");
        System.loadLibrary("memoryintarraytest");
    }

    @Test
    public void testSize() throws Exception {
        MemoryIntArray array = null;
        try {
            array = new MemoryIntArray(3);
            assertEquals("size must be three", 3, array.size());
        } finally {
            IoUtils.closeQuietly(array);
        }
    }

    @Test
    public void testGetSet() throws Exception {
        MemoryIntArray array = null;
        try {
            array = new MemoryIntArray(3);

            array.set(0, 1);
            array.set(1, 2);
            array.set(2, 3);

            assertEquals("First element should be 1", 1, array.get(0));
            assertEquals("First element should be 2", 2, array.get(1));
            assertEquals("First element should be 3", 3, array.get(2));
        } finally {
            IoUtils.closeQuietly(array);
        }
    }

    @Test
    public void testWritable() throws Exception {
        MemoryIntArray array = null;
        try {
            array = new MemoryIntArray(3);
            assertTrue("Must be mutable", array.isWritable());
        } finally {
            IoUtils.closeQuietly(array);
        }
    }

    @Test
    public void testClose() throws Exception {
        MemoryIntArray array = null;
        try {
            array = new MemoryIntArray(3);
            array.close();
            assertTrue("Must be closed", array.isClosed());
        } finally {
            if (array != null && !array.isClosed()) {
                IoUtils.closeQuietly(array);
            }
        }
    }

    @Test
    public void testMarshalledGetSet() throws Exception {
        MemoryIntArray firstArray = null;
        MemoryIntArray secondArray = null;
        try {
            firstArray = new MemoryIntArray(3);

            firstArray.set(0, 1);
            firstArray.set(1, 2);
            firstArray.set(2, 3);

            Parcel parcel = Parcel.obtain();
            parcel.writeParcelable(firstArray, 0);
            parcel.setDataPosition(0);
            secondArray = parcel.readParcelable(null);
            parcel.recycle();

            assertNotNull("Should marshall file descriptor", secondArray);

            assertEquals("First element should be 1", 1, secondArray.get(0));
            assertEquals("First element should be 2", 2, secondArray.get(1));
            assertEquals("First element should be 3", 3, secondArray.get(2));
        } finally {
            IoUtils.closeQuietly(firstArray);
            IoUtils.closeQuietly(secondArray);
        }
    }

    @Test
    public void testInteractOnceClosed() throws Exception {
        MemoryIntArray array = null;
        try {
            array = new MemoryIntArray(3);
            array.close();

            array.close();

            try {
                array.size();
                fail("Cannot interact with a closed instance");
            } catch (IllegalStateException e) {
                /* expected */
            }

            try {
                array.get(0);
                fail("Cannot interact with a closed instance");
            } catch (IllegalStateException e) {
                /* expected */
            }

            try {
                array.set(0, 1);
                fail("Cannot interact with a closed instance");
            } catch (IllegalStateException e) {
                /* expected */
            }

            try {
                array.isWritable();
                fail("Cannot interact with a closed instance");
            } catch (IllegalStateException e) {
                /* expected */
            }
        } finally {
            if (array != null && !array.isClosed()) {
                IoUtils.closeQuietly(array);
            }
        }
    }

    @Test
    public void testInteractPutOfBounds() throws Exception {
        MemoryIntArray array = null;
        try {
            array = new MemoryIntArray(3);

            try {
                array.get(-1);
                fail("Cannot interact out of array bounds");
            } catch (IndexOutOfBoundsException e) {
                /* expected */
            }

            try {
                array.get(3);
                fail("Cannot interact out of array bounds");
            } catch (IndexOutOfBoundsException e) {
                /* expected */
            }

            try {
                array.set(-1, 0);
                fail("Cannot interact out of array bounds");
            } catch (IndexOutOfBoundsException e) {
                /* expected */
            }

            try {
                array.set(3, 0);
                fail("Cannot interact out of array bounds");
            } catch (IndexOutOfBoundsException e) {
                /* expected */
            }
        } finally {
            IoUtils.closeQuietly(array);
        }
    }

    @Test
    public void testOverMaxSize() throws Exception {
        MemoryIntArray array = null;
        try {
            array = new MemoryIntArray(MemoryIntArray.getMaxSize() + 1);
            fail("Cannot use over max size");
        } catch (IllegalArgumentException e) {
            /* expected */
        } finally {
            IoUtils.closeQuietly(array);
        }
    }

    @Test
    public void testNotMutableByUnprivilegedClients() throws Exception {
        RemoteIntArray remoteIntArray = new RemoteIntArray(1);
        try {
            assertNotNull("Couldn't get remote instance", remoteIntArray);
            MemoryIntArray localIntArray = remoteIntArray.peekInstance();
            assertNotNull("Couldn't get local instance", localIntArray);

            remoteIntArray.set(0, 1);
            assertSame("Remote should be able to modify", 1, remoteIntArray.get(0));

            try {
                localIntArray.set(0, 0);
                fail("Local shouldn't be able to modify");
            } catch (UnsupportedOperationException e) {
                /* expected */
            }
            assertSame("Local shouldn't be able to modify", 1, localIntArray.get(0));
            assertSame("Local shouldn't be able to modify", 1, remoteIntArray.get(0));
        } finally {
            remoteIntArray.destroy();
        }
    }

    @Test
    public void testAshmemSizeMatchesMemoryIntArraySize() throws Exception {
        boolean success = false;

        // Get a handle to a remote process to send the fd
        RemoteIntArray remoteIntArray = new RemoteIntArray(1);
        try {
            // Let us try 100 times
            for (int i = 0; i < 100; i++) {
                // Create a MemoryIntArray to muck with
                MemoryIntArray array = new MemoryIntArray(1);

                // Create the fd to stuff in the MemoryIntArray
                final int fd = nativeCreateAshmem("foo", 1);

                // Replace the fd with our ahsmem region
                Field fdFiled = MemoryIntArray.class.getDeclaredField("mFd");
                fdFiled.setAccessible(true);
                fdFiled.set(array, fd);

                CountDownLatch countDownLatch = new CountDownLatch(2);

                new Thread() {
                    @Override
                    public void run() {
                        for (int i = 2; i < Integer.MAX_VALUE; i++) {
                            if (countDownLatch.getCount() == 1) {
                                countDownLatch.countDown();
                                return;
                            }
                            nativeSetAshmemSize(fd, i);
                        }
                    }
                }.start();

                try {
                    remoteIntArray.accessLastElementInRemoteProcess(array);
                } catch (IllegalArgumentException e) {
                    success = true;
                }

                countDownLatch.countDown();
                countDownLatch.await(1000, TimeUnit.MILLISECONDS);

                if (success) {
                    break;
                }
            }
        } finally {
            remoteIntArray.destroy();
        }

        if (!success) {
            fail("MemoryIntArray should catch ahshmem size changing under it");
        }
    }

    private native int nativeCreateAshmem(String name, int size);
    private native void nativeSetAshmemSize(int fd, int size);
}
