/*
 * Copyright (C) 2014 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.ddmlib.allocations;

import com.android.ddmlib.AllocationInfo;
import com.android.ddmlib.AllocationsParser;
import com.google.common.base.Charsets;
import junit.framework.TestCase;

import java.io.IOException;
import java.net.URL;
import java.nio.ByteBuffer;

public class AllocationsParserTest extends TestCase {

  public void testParsingOnNoAllocations() throws IOException {
    ByteBuffer data = putAllocationInfo(new String[0], new String[0], new String[0], new int[0][], new short[0][][]);
    assertEquals(0, AllocationsParser.parse(data).length);
  }

  public void testParsingOnOneAllocationWithoutStackTrace() throws IOException {
    ByteBuffer data =
            putAllocationInfo(new String[]{"path.Foo"}, new String[0], new String[0], new int[][]{{32, 4, 0, 0}}, new short[][][]{{}});
    AllocationInfo[] info = AllocationsParser.parse(data);
    assertEquals(1, info.length);

    AllocationInfo alloc = info[0];
    checkEntry(1, "path.Foo", 32, 4, alloc);
    checkFirstTrace(null, null, alloc);
    assertEquals(0, alloc.getStackTrace().length);
  }

  public void testParsingOnOneAllocationWithStackTrace() throws IOException {
    ByteBuffer data = putAllocationInfo(new String[]{"path.Foo", "path.Bar", "path.Baz"}, new String[]{"foo", "bar", "baz"},
            new String[]{"Foo.java", "Bar.java"}, new int[][]{{64, 0, 1, 3}},
            new short[][][]{{{1, 1, 1, -1}, {2, 0, 1, 2000}, {0, 2, 0, 10}}});
    AllocationInfo[] info = AllocationsParser.parse(data);
    assertEquals(1, info.length);

    AllocationInfo alloc = info[0];
    checkEntry(1, "path.Bar", 64, 0, alloc);
    checkFirstTrace("path.Bar", "bar", alloc);

    StackTraceElement[] elems = alloc.getStackTrace();
    assertEquals(3, elems.length);

    checkStackFrame("path.Bar", "bar", "Bar.java", -1, elems[0]);
    checkStackFrame("path.Baz", "foo", "Bar.java", 2000, elems[1]);
    checkStackFrame("path.Foo", "baz", "Foo.java", 10, elems[2]);
  }

  public void testParsing() throws IOException {
    ByteBuffer data = putAllocationInfo(new String[]{"path.Red", "path.Green", "path.Blue", "path.LightCanaryishGrey"},
            new String[]{"eatTiramisu", "failUnitTest", "watchCatVideos", "passGo", "collectFakeMoney", "findWaldo"},
            new String[]{"Red.java", "SomewhatBlue.java", "LightCanaryishGrey.java"},
            new int[][]{{128, 8, 0, 2}, {16, 8, 2, 1}, {42, 2, 1, 3}},
            new short[][][]{{{1, 0, 1, 100}, {2, 5, 1, -2}}, {{0, 1, 0, -1}}, {{3, 4, 2, 10001}, {0, 3, 0, 0}, {2, 2, 1, 16}}});
    AllocationInfo[] info = AllocationsParser.parse(data);
    assertEquals(3, info.length);

    AllocationInfo alloc1 = info[0];
    checkEntry(3, "path.Red", 128, 8, alloc1);
    checkFirstTrace("path.Green", "eatTiramisu", alloc1);

    StackTraceElement[] elems1 = alloc1.getStackTrace();
    assertEquals(2, elems1.length);

    checkStackFrame("path.Green", "eatTiramisu", "SomewhatBlue.java", 100, elems1[0]);
    checkStackFrame("path.Blue", "findWaldo", "SomewhatBlue.java", -2, elems1[1]);

    AllocationInfo alloc2 = info[1];
    checkEntry(2, "path.Blue", 16, 8, alloc2);
    checkFirstTrace("path.Red", "failUnitTest", alloc2);

    StackTraceElement[] elems2 = alloc2.getStackTrace();
    assertEquals(1, elems2.length);

    checkStackFrame("path.Red", "failUnitTest", "Red.java", -1, elems2[0]);

    AllocationInfo alloc3 = info[2];
    checkEntry(1, "path.Green", 42, 2, alloc3);
    checkFirstTrace("path.LightCanaryishGrey", "collectFakeMoney", alloc3);

    StackTraceElement[] elems3 = alloc3.getStackTrace();
    assertEquals(3, elems3.length);

    checkStackFrame("path.LightCanaryishGrey", "collectFakeMoney", "LightCanaryishGrey.java", 10001, elems3[0]);
    checkStackFrame("path.Red", "passGo", "Red.java", 0, elems3[1]);
    checkStackFrame("path.Blue", "watchCatVideos", "SomewhatBlue.java", 16, elems3[2]);
  }

  private static void checkEntry(int order, String className, int size, int thread, AllocationInfo alloc) {
    assertEquals(order, alloc.getAllocNumber());
    assertEquals(className, alloc.getAllocatedClass());
    assertEquals(size, alloc.getSize());
    assertEquals(thread, alloc.getThreadId());
  }

  private static void checkFirstTrace(String className, String methodName, AllocationInfo alloc) {
    assertEquals(className, alloc.getFirstTraceClassName());
    assertEquals(methodName, alloc.getFirstTraceMethodName());
  }

  private static void checkStackFrame(String className, String methodName, String fileName, int lineNumber, StackTraceElement elem) {
    assertEquals(className, elem.getClassName());
    assertEquals(methodName, elem.getMethodName());
    assertEquals(fileName, elem.getFileName());
    assertEquals(lineNumber, elem.getLineNumber());
  }

  public static ByteBuffer putAllocationInfo(String[] classNames, String[] methodNames, String[] fileNames, int[][] entries,
                                                short[][][] stackFrames) throws IOException {
    byte msgHdrLen = 15, entryHdrLen = 9, stackFrameLen = 8;

    // Number of bytes from start of message to string tables
    int offset = msgHdrLen;
    for (int[] entry : entries) {
      offset += entryHdrLen + (stackFrameLen * entry[3]);
    }

    // Number of bytes in string tables
    int strNamesLen = 0;
    for (String name : classNames) { strNamesLen += 4 + (2 * name.length()); }
    for (String name : methodNames) { strNamesLen += 4 + (2 * name.length()); }
    for (String name : fileNames) { strNamesLen += 4 + (2 * name.length()); }

    ByteBuffer data = ByteBuffer.allocate(offset + strNamesLen);

    data.put(new byte[]{msgHdrLen, entryHdrLen, stackFrameLen});
    data.putShort((short) entries.length);
    data.putInt(offset);
    data.putShort((short) classNames.length);
    data.putShort((short) methodNames.length);
    data.putShort((short) fileNames.length);

    for (short i = 0; i < entries.length; ++i) {
      data.putInt(entries[i][0]); // total alloc size
      data.putShort((short) entries[i][1]); // thread id
      data.putShort((short) entries[i][2]); // allocated class index
      data.put((byte) entries[i][3]); // stack depth

      short[][] frames = stackFrames[i];
      for (short[] frame : frames) {
        data.putShort(frame[0]); // class name
        data.putShort(frame[1]); // method name
        data.putShort(frame[2]); // source file
        data.putShort(frame[3]); // line number
      }
    }

    for (String name : classNames) {
      data.putInt(name.length());
      data.put(strToBytes(name));
    }
    for (String name : methodNames) {
      data.putInt(name.length());
      data.put(strToBytes(name));
    }
    for (String name : fileNames) {
      data.putInt(name.length());
      data.put(strToBytes(name));
    }
    data.rewind();
    return data;
  }

  private static byte[] strToBytes(String str) {
    return str.getBytes(Charsets.UTF_16BE);
  }
}