package org.altbeacon.beacon;

import org.altbeacon.beacon.logging.LogManager;
import org.altbeacon.beacon.logging.Loggers;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

import java.util.Arrays;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;

@Config(sdk = 28)

@RunWith(RobolectricTestRunner.class)

/*
HOW TO SEE DEBUG LINES FROM YOUR UNIT TESTS:
1. set a line like this at the start of your test:
           org.robolectric.shadows.ShadowLog.stream = System.err;
2. run the tests from the command line
3. Look at the test report file in your web browser, e.g.
   file:///Users/dyoung/workspace/AndroidProximityLibrary/build/reports/tests/index.html
4. Expand the System.err section
 */
public class BeaconParserTest {

    public static byte[] hexStringToByteArray(String s) {
        int len = s.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                    + Character.digit(s.charAt(i+1), 16));
        }
        return data;
    }
    public static String byteArrayToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            sb.append(String.format("%02x", bytes[i]));
        }
        return sb.toString();
    }

    @Test
    public void testSetBeaconLayout() {
        byte[] bytes = hexStringToByteArray("02011a1bffbeac2f234454cf6d4a0fadf2f4911ba9ffa600010002c509000000");
        BeaconParser parser = new BeaconParser();
        parser.setBeaconLayout("m:2-3=beac,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25");

        assertEquals("parser should get beacon type code start offset", new Integer(2), parser.mMatchingBeaconTypeCodeStartOffset);
        assertEquals("parser should get beacon type code end offset",  new Integer(3), parser.mMatchingBeaconTypeCodeEndOffset);
        assertEquals("parser should get beacon type code", new Long(0xbeac), parser.getMatchingBeaconTypeCode());
        assertEquals("parser should get identifier start offset", new Integer(4), parser.mIdentifierStartOffsets.get(0));
        assertEquals("parser should get identifier end offset", new Integer(19), parser.mIdentifierEndOffsets.get(0));
        assertEquals("parser should get identifier start offset", new Integer(20), parser.mIdentifierStartOffsets.get(1));
        assertEquals("parser should get identifier end offset", new Integer(21), parser.mIdentifierEndOffsets.get(1));
        assertEquals("parser should get identifier start offset", new Integer(22), parser.mIdentifierStartOffsets.get(2));
        assertEquals("parser should get identifier end offset", new Integer(23), parser.mIdentifierEndOffsets.get(2));
        assertEquals("parser should get power start offset", new Integer(24), parser.mPowerStartOffset);
        assertEquals("parser should get power end offset", new Integer(24), parser.mPowerEndOffset);
        assertEquals("parser should get data start offset", new Integer(25), parser.mDataStartOffsets.get(0));
        assertEquals("parser should get data end offset", new Integer(25), parser.mDataEndOffsets.get(0));

    }

    @Test
    public void testLongToByteArray() {
        BeaconParser parser = new BeaconParser();
        byte[] bytes = parser.longToByteArray(10, 1);
        assertEquals("first byte should be 10", 10, bytes[0]);
    }

    @Test
    public void testRecognizeBeacon() {
        LogManager.setLogger(Loggers.verboseLogger());
        org.robolectric.shadows.ShadowLog.stream = System.err;
        byte[] bytes = hexStringToByteArray("02011a1aff180112342f234454cf6d4a0fadf2f4911ba9ffa600010002c5");
        BeaconParser parser = new BeaconParser();
        parser.setBeaconLayout("m:2-3=1234,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25");
        Beacon beacon = parser.fromScanData(bytes, -55, null, 123456L);
        assertEquals("mRssi should be as passed in", -55, beacon.getRssi());
        assertEquals("uuid should be parsed", "2f234454-cf6d-4a0f-adf2-f4911ba9ffa6", beacon.getIdentifier(0).toString());
        assertEquals("id2 should be parsed", "1", beacon.getIdentifier(1).toString());
        assertEquals("id3 should be parsed", "2", beacon.getIdentifier(2).toString());
        assertEquals("txPower should be parsed", -59, beacon.getTxPower());
        assertEquals("manufacturer should be parsed", 0x118 ,beacon.getManufacturer());
    }

    @Test
    public void testAllowsAccessToParserIdentifier() {
        LogManager.setLogger(Loggers.verboseLogger());
        org.robolectric.shadows.ShadowLog.stream = System.err;
        byte[] bytes = hexStringToByteArray("02011a1aff180112342f234454cf6d4a0fadf2f4911ba9ffa600010002c5");
        BeaconParser parser = new BeaconParser("my_beacon_type");
        parser.setBeaconLayout("m:2-3=1234,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25");
        Beacon beacon = parser.fromScanData(bytes, -55, null, 123456L);
        assertEquals("parser identifier should be accessible", "my_beacon_type", beacon.getParserIdentifier());
    }

    @Test
    public void testParsesBeaconMissingDataField() {
        LogManager.setLogger(Loggers.verboseLogger());
        org.robolectric.shadows.ShadowLog.stream = System.err;
        byte[] bytes = hexStringToByteArray("02011a1aff1801beac2f234454cf6d4a0fadf2f4911ba9ffa600010002c5000000");
        BeaconParser parser = new BeaconParser();
        parser.setBeaconLayout("m:2-3=beac,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25");
        Beacon beacon = parser.fromScanData(bytes, -55, null, 123456L);
        assertEquals("mRssi should be as passed in", -55, beacon.getRssi());
        assertEquals("uuid should be parsed", "2f234454-cf6d-4a0f-adf2-f4911ba9ffa6", beacon.getIdentifier(0).toString());
        assertEquals("id2 should be parsed", "1", beacon.getIdentifier(1).toString());
        assertEquals("id3 should be parsed", "2", beacon.getIdentifier(2).toString());
        assertEquals("txPower should be parsed", -59, beacon.getTxPower());
        assertEquals("manufacturer should be parsed", 0x118 ,beacon.getManufacturer());
        assertEquals("missing data field zero should be zero", new Long(0l), beacon.getDataFields().get(0));

    }


    @Test
    public void testRecognizeBeaconWithFormatSpecifyingManufacturer() {
        LogManager.setLogger(Loggers.verboseLogger());
        org.robolectric.shadows.ShadowLog.stream = System.err;
        byte[] bytes = hexStringToByteArray("02011a1bff1801beac2f234454cf6d4a0fadf2f4911ba9ffa600010002c509000000");
        BeaconParser parser = new BeaconParser();
        parser.setBeaconLayout("m:0-3=1801beac,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25");
        Beacon beacon = parser.fromScanData(bytes, -55, null, 123456L);
        assertEquals("mRssi should be as passed in", -55, beacon.getRssi());
        assertEquals("uuid should be parsed", "2f234454-cf6d-4a0f-adf2-f4911ba9ffa6", beacon.getIdentifier(0).toString());
        assertEquals("id2 should be parsed", "1", beacon.getIdentifier(1).toString());
        assertEquals("id3 should be parsed", "2", beacon.getIdentifier(2).toString());
        assertEquals("txPower should be parsed", -59, beacon.getTxPower());
        assertEquals("manufacturer should be parsed", 0x118 ,beacon.getManufacturer());
    }

    @Test
    public void testReEncodesBeacon() {
        org.robolectric.shadows.ShadowLog.stream = System.err;
        byte[] bytes = hexStringToByteArray("02011a1bff1801beac2f234454cf6d4a0fadf2f4911ba9ffa600010002c509");
        BeaconParser parser = new BeaconParser();
        parser.setBeaconLayout("m:2-3=beac,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25");
        Beacon beacon = parser.fromScanData(bytes, -55, null, 123456L);
        byte[] regeneratedBytes = parser.getBeaconAdvertisementData(beacon);
        byte[] expectedMatch = Arrays.copyOfRange(bytes, 7, bytes.length);
        assertArrayEquals("beacon advertisement bytes should be the same after re-encoding", expectedMatch, regeneratedBytes);
    }

    @Test
    public void testReEncodesBeaconForEddystoneTelemetry() {
        org.robolectric.shadows.ShadowLog.stream = System.err;
        byte[] bytes = hexStringToByteArray("0201060303aafe1516aafe2001021203130414243405152535");
        BeaconParser parser = new BeaconParser();
        parser.setBeaconLayout(BeaconParser.EDDYSTONE_TLM_LAYOUT);
        Beacon beacon = parser.fromScanData(bytes, -55, null, 123456L);
        byte[] regeneratedBytes = parser.getBeaconAdvertisementData(beacon);
        byte[] expectedMatch = Arrays.copyOfRange(bytes, 11, bytes.length);
        assertEquals("beacon advertisement bytes should be the same after re-encoding", byteArrayToHexString(expectedMatch), byteArrayToHexString(regeneratedBytes));
    }

    @Test
    public void testLittleEndianIdentifierParsing() {
        org.robolectric.shadows.ShadowLog.stream = System.err;
        byte[] bytes = hexStringToByteArray("02011a1bff1801beac0102030405060708090a0b0c0d0e0f1011121314c50900000000");
        BeaconParser parser = new BeaconParser();
        parser.setBeaconLayout("m:2-3=beac,i:4-9,i:10-15l,i:16-23,p:24-24,d:25-25");
        Beacon beacon = parser.fromScanData(bytes, -55, null, 123456L);
        assertEquals("mRssi should be as passed in", -55, beacon.getRssi());
        assertEquals("id1 should be big endian", "0x010203040506", beacon.getIdentifier(0).toString());
        assertEquals("id2 should be little endian", "0x0c0b0a090807", beacon.getIdentifier(1).toString());
        assertEquals("id3 should be big endian", "0x0d0e0f1011121314", beacon.getIdentifier(2).toString());
        assertEquals("txPower should be parsed", -59, beacon.getTxPower());
        assertEquals("manufacturer should be parsed", 0x118, beacon.getManufacturer());
    }

    @Test
    public void testReEncodesLittleEndianBeacon() {
        org.robolectric.shadows.ShadowLog.stream = System.err;
        byte[] bytes = hexStringToByteArray("02011a1bff1801beac0102030405060708090a0b0c0d0e0f1011121314c509");
        BeaconParser parser = new BeaconParser();
        parser.setBeaconLayout("m:2-3=beac,i:4-9,i:10-15l,i:16-23,p:24-24,d:25-25");
        Beacon beacon = parser.fromScanData(bytes, -55, null, 123456L);
        byte[] regeneratedBytes = parser.getBeaconAdvertisementData(beacon);
        byte[] expectedMatch = Arrays.copyOfRange(bytes, 7, bytes.length);
        System.err.println(byteArrayToHexString(expectedMatch));
        System.err.println(byteArrayToHexString(regeneratedBytes));
        assertEquals("beacon advertisement bytes should be the same after re-encoding", byteArrayToHexString(expectedMatch), byteArrayToHexString(regeneratedBytes));
    }


    @Test
    public void testRecognizeBeaconCapturedManufacturer() {
        org.robolectric.shadows.ShadowLog.stream = System.err;
        byte[] bytes = hexStringToByteArray("0201061bffaabbbeace2c56db5dffb48d2b060d0f5a71096e000010004c50000000000000000000000000000000000000000000000000000000000000000");
        BeaconParser parser = new BeaconParser();
        parser.setBeaconLayout("m:2-3=beac,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25");
        Beacon beacon = parser.fromScanData(bytes, -55, null, 123456L);
        assertEquals("manufacturer should be parsed", "bbaa", String.format("%04x", beacon.getManufacturer()));
    }


    @Test
    public void testParseGattIdentifierThatRunsOverPduLength() {
        org.robolectric.shadows.ShadowLog.stream = System.err;
        byte[] bytes = hexStringToByteArray("0201060303aafe0d16aafe10e702676f6f676c65000c09526164426561636f6e204700000000000000000000000000000000000000000000000000000000");
        BeaconParser parser = new BeaconParser();
        parser.setAllowPduOverflow(false);
        parser.setBeaconLayout("s:0-1=feaa,m:2-2=10,p:3-3:-41,i:4-20");
        Beacon beacon = parser.fromScanData(bytes, -55, null, 123456L);
        assertNull("beacon should not be parsed", beacon);
    }

    @Test
    public void testLongUrlBeaconIdentifier() {
        org.robolectric.shadows.ShadowLog.stream = System.err;
        byte[] bytes = hexStringToByteArray("0201060303aafe0d16aafe10e70102030405060708090a0b0c0d0e0f0102030405060708090a0b0c0d0e0f00000000000000000000000000000000000000");
        BeaconParser parser = new BeaconParser();
        parser.setBeaconLayout("s:0-1=feaa,m:2-2=10,p:3-3:-41,i:4-20v");
        Beacon beacon = parser.fromScanData(bytes, -55, null, 123456L);
        assertEquals("URL Identifier should be truncated at 8 bytes", 8, beacon.getId1().toByteArray().length);
    }

    @Test
    public void testParseManufacturerIdentifierThatRunsOverPduLength() {
        org.robolectric.shadows.ShadowLog.stream = System.err;

        // Note that the length field below is 0x16 instead of 0x1b, indicating that the packet ends
        // one byte before the second identifier field starts
        byte[] bytes = hexStringToByteArray("02011a16ff1801beac2f234454cf6d4a0fadf2f4911ba9ffa600010002c509000000");
        BeaconParser parser = new BeaconParser();
        parser.setAllowPduOverflow(false);
        parser.setBeaconLayout("m:2-3=beac,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25");

        Beacon beacon = parser.fromScanData(bytes, -55, null, 123456L);
        assertNull("beacon should not be parsed", beacon);
    }

    @Test
    public void testParseProblematicBeaconFromIssue229() {
        org.robolectric.shadows.ShadowLog.stream = System.err;

        // Note that the length field below is 0x16 instead of 0x1b, indicating that the packet ends
        // one byte before the second identifier field starts

        byte[] bytes = hexStringToByteArray("0201061bffe000beac7777772e626c756b692e636f6d000100010001abaa000000");
        BeaconParser parser = new BeaconParser();
        parser.setBeaconLayout("m:2-3=beac,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25");

        Beacon beacon = parser.fromScanData(bytes, -55, null, 123456L);
        assertNotNull("beacon should be parsed", beacon);
    }


    @Test
    public void testCanParseLocationBeacon() {
        org.robolectric.shadows.ShadowLog.stream = System.err;
        BeaconManager.setDebug(true);

        double latitude = 38.93;
        double longitude = -77.23;
        Beacon beacon = new Beacon.Builder()
                .setManufacturer(0x0118) // Radius Networks
                .setId1("1") // device sequence number
                .setId2(String.format("0x%08X", (long)((latitude+90)*10000.0)))
                .setId3(String.format("0x%08X", (long)((longitude+180)*10000.0)))
                .setTxPower(-59) // The measured transmitter power at one meter in dBm
                .build();
        // TODO: make this pass if data fields are little endian or > 4 bytes (or even > 2 bytes)
        BeaconParser p = new BeaconParser().
                setBeaconLayout("m:2-3=10ca,i:4-9,i:10-13,i:14-17,p:18-18");
        byte[] bytes = p.getBeaconAdvertisementData(beacon);
        byte[] headerBytes = hexStringToByteArray("02011a1bff1801");
        byte[] advBytes = new byte[bytes.length+headerBytes.length];
        System.arraycopy(headerBytes, 0, advBytes, 0, headerBytes.length);
        System.arraycopy(bytes, 0, advBytes, headerBytes.length, bytes.length);

        Beacon parsedBeacon = p.fromScanData(advBytes, -59, null, 123456L);
        assertNotNull(String.format("Parsed beacon from %s should not be null", byteArrayToHexString(advBytes)), parsedBeacon);
        double parsedLatitude = Long.parseLong(parsedBeacon.getId2().toString().substring(2), 16) / 10000.0 - 90.0;
        double parsedLongitude = Long.parseLong(parsedBeacon.getId3().toString().substring(2), 16) / 10000.0 - 180.0;

        long encodedLatitude = (long)((latitude+90)*10000.0);
        assertEquals("encoded latitude hex should match", String.format("0x%08x", encodedLatitude), parsedBeacon.getId2().toString());
        assertEquals("device sequence num should be same", "0x000000000001", parsedBeacon.getId1().toString());
        assertEquals("latitude should be about right", latitude, parsedLatitude, 0.0001);
        assertEquals("longitude should be about right", longitude, parsedLongitude, 0.0001);

    }
    @Test
    public void testCanGetAdvertisementDataForUrlBeacon() {
        org.robolectric.shadows.ShadowLog.stream = System.err;
        BeaconManager.setDebug(true);
        Beacon beacon = new Beacon.Builder()
                .setManufacturer(0x0118)
                .setId1("02646576656c6f7065722e636f6d") // http://developer.com
                .setTxPower(-59) // The measured transmitter power at one meter in dBm
                .build();
        BeaconParser p = new BeaconParser().
                setBeaconLayout("s:0-1=feaa,m:2-2=10,p:3-3:-41,i:4-20v");
        byte[] bytes = p.getBeaconAdvertisementData(beacon);
        assertEquals("First byte of url should be in position 3", 0x02, bytes[2]);
    }
    @Test
    public void doesNotCashWithOverflowingByteCodeComparisonOnPdu() {
        // Test for https://github.com/AltBeacon/android-beacon-library/issues/323
        org.robolectric.shadows.ShadowLog.stream = System.err;

        // Note that the length field below is 0x16 instead of 0x1b, indicating that the packet ends
        // one byte before the second identifier field starts

        byte[] bytes = hexStringToByteArray("02010604ffe000be");
        BeaconParser parser = new BeaconParser();
        parser.setBeaconLayout("m:2-3=beac,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25");

        Beacon beacon = parser.fromScanData(bytes, -55, null, 123456L);
        assertNull("beacon not be parsed without an exception being thrown", beacon);
    }

    @Test
    public void testCanParseLongDataTypeOfDifferentSize(){
        // Create a beacon parser
        BeaconParser parser = new BeaconParser();
        parser.setBeaconLayout("m:2-3=0118,i:4-7,p:8-8,d:9-16,d:18-21,d:22-25");

        // Generate sample beacon for test purpose.
        java.util.List<Long> sampleData = new java.util.ArrayList<Long>();
        Long now = System.currentTimeMillis();
        sampleData.add(now);
        sampleData.add(1234L);
        sampleData.add(9876L);
        Beacon beacon = new Beacon.Builder()
                .setManufacturer(0x0118)
                .setId1("02646576656c6f7065722e636f6d")
                .setTxPower(-59)
                .setDataFields(sampleData)
                .build();

        assertEquals("beacon contains a valid data on index 0", now, beacon.getDataFields().get(0));

        // Make byte array
        byte[] headerBytes = hexStringToByteArray("1bff1801");
        byte[] bodyBytes = parser.getBeaconAdvertisementData(beacon);
        byte[] bytes = new byte[headerBytes.length + bodyBytes.length];
        System.arraycopy(headerBytes, 0, bytes, 0, headerBytes.length);
        System.arraycopy(bodyBytes, 0, bytes, headerBytes.length, bodyBytes.length);

        // Try parsing the byte array
        Beacon parsedBeacon = parser.fromScanData(bytes, -59, null, 123456L);

        assertEquals("parsed beacon should contain a valid data on index 0", now, parsedBeacon.getDataFields().get(0));
        assertEquals("parsed beacon should contain a valid data on index 1", Long.valueOf(1234L), parsedBeacon.getDataFields().get(1));
        assertEquals("parsed beacon should contain a valid data on index 2", Long.valueOf(9876L), parsedBeacon.getDataFields().get(2));

    }
}