/*
 * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import org.testng.SkipException;

import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.CharBuffer;
import java.util.Arrays;
import java.util.HexFormat;
import java.util.Locale;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertSame;
import static org.testng.Assert.assertThrows;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.expectThrows;

/*
 * @test
 * @summary Check HexFormat formatting and parsing
 * @run testng/othervm HexFormatTest
 */

@Test
public class HexFormatTest {
    static final Class<NullPointerException> NPE = NullPointerException.class;

    @DataProvider(name = "HexFormattersParsers")
    Object[][] hexFormattersParsers() {
        return new Object[][]{
                {"", "", "", true,
                        HexFormat.of().withUpperCase()},
                {", ", "#", "L", false,
                        HexFormat.ofDelimiter(", ").withPrefix("#").withSuffix("L")},
                {"", "", "", false,
                        HexFormat.of().withPrefix("").withSuffix("")},
                {".", "", "", false,
                        HexFormat.ofDelimiter(".").withPrefix("").withSuffix("")},
                {", ", "0x", "", true,
                        HexFormat.ofDelimiter(", ").withUpperCase().withPrefix("0x")},
                {"\u0202", "\u0203", "\u0204", false,
                        HexFormat.ofDelimiter("\u0202").withPrefix("\u0203").withSuffix("\u0204")},
                {"\u0202", "", "", false,
                        HexFormat.ofDelimiter("\u0202")},

        };
    }

    @DataProvider(name = "HexStringsThrowing")
    Object[][] HexStringsThrowing() {
        return new Object[][]{
                {"0", ":", "", ""},         // wrong string length
                {"01:", ":", "", ""},       // wrong string length
                {"01:0", ":", "", ""},      // wrong string length
                {"0", ",", "", ""},         // wrong length and separator
                {"01:", ",", "", ""},       // wrong length and separator
                {"01:0", ",", "", ""},      // wrong length and separator
                {"01:00", ",", "", ""},     // wrong separator
                {"00]", ",", "[", "]"},     // missing prefix
                {"[00", ",", "[", "]"},     // missing suffix
                {"]", ",", "[", "]"},       // missing prefix
                {"[", ",", "[", "]"},       // missing suffix
                {"00", ",", "abc", ""},     // Prefix longer than string
                {"01", ",", "", "def"},     // Suffix longer than string
                {"abc00,", ",", "abc", ""},     // Prefix and delim but not another value
                {"01def,", ",", "", "def"},     // Suffix and delim but not another value
        };
    }

    @DataProvider(name = "BadBytesThrowing")
    Object[][] badBytesThrowing() {
        return new Object[][]{
                {new byte[1], 0, 2},        // bad toIndex
                {new byte[1], 1, 2},        // bad fromIndex + toIndex
                {new byte[1], -1, 2},       // bad fromIndex
                {new byte[1], -1, 1},       // bad fromIndex
                {new byte[1], 0, -1},       // bad toIndex
                {new byte[1], 1, -1},       // bad toIndex
        };
    }

    @DataProvider(name = "BadParseHexThrowing")
    Object[][] badParseHexThrowing() {
        return new Object[][]{
                {"a", 0, 2, IndexOutOfBoundsException.class},        // bad toIndex
                {"b", 1, 2, IndexOutOfBoundsException.class},        // bad toIndex
                {"a", -1, 2, IndexOutOfBoundsException.class},       // bad fromIndex
                {"b", -1, 1, IndexOutOfBoundsException.class},       // bad fromIndex
                {"a", 0, -1, IndexOutOfBoundsException.class},       // bad toIndex
                {"b", 1, -1, IndexOutOfBoundsException.class},       // bad fromIndex + toIndex
                {"76543210", 0, 7, IllegalArgumentException.class},  // odd number of digits
                {"zz00", 0, 4, IllegalArgumentException.class},      // non-hex digits
                {"00zz", 0, 4, IllegalArgumentException.class},      // non-hex digits
        };
    }

    @DataProvider(name = "BadFromHexDigitsThrowing")
    Object[][] badHexDigitsThrowing() {
        return new Object[][]{
                {"a", 0, 2, IndexOutOfBoundsException.class},        // bad toIndex
                {"b", 1, 2, IndexOutOfBoundsException.class},        // bad fromIndex + toIndex
                {"a", -1, 2, IndexOutOfBoundsException.class},       // bad toIndex
                {"b", -1, 1, IndexOutOfBoundsException.class},       // bad fromIndex + toIndex
                {"a", 0, -1, IndexOutOfBoundsException.class},       // bad toIndex
                {"b", 1, -1, IndexOutOfBoundsException.class},       // bad fromIndex + toIndex
        };
    }

    static byte[] genBytes(int origin, int len) {
        byte[] bytes = new byte[len];
        for (int i = 0; i < len; i++)
            bytes[i] = (byte) (origin + i);
        return bytes;
    }

    @Test
    static void testToHex() {
        HexFormat hex = HexFormat.of();
        for (int i = 0; i < 32; i++) {
            char c = hex.toLowHexDigit((byte)i);
            String expected = Integer.toHexString(i & 0xf);
            assertEquals(c, expected.charAt(0), "toHex formatting");
        }
    }

    @Test
    static void testToHexDigits() {
        HexFormat hex = HexFormat.of();
        for (int i = 0; i < 256; i++) {
            String actual = hex.toHexDigits((byte)i);
            int expected = HexFormat.fromHexDigits(actual);
            assertEquals(expected, i, "fromHexDigits");
            assertEquals(actual.charAt(0), hex.toHighHexDigit((byte)i),
                    "first char mismatch");
            assertEquals(actual.charAt(1), hex.toLowHexDigit((byte)i),
                    "second char mismatch");
        }
    }

    @Test
    static void testIsHexDigit() {
        for (int i = 0; i < 0x3ff; i++) {
            boolean actual = HexFormat.isHexDigit(i);
            boolean expected = Character.digit(i, 16) >= 0;
            assertEquals(actual, expected, "isHexDigit: " + i);
        }
    }

    @Test
    static void testFromHexDigit() {
        String chars = "0123456789ABCDEF0123456789abcdef";
        for (int i = 0; i < chars.length(); i++) {
            int v = HexFormat.fromHexDigit(chars.charAt(i));
            assertEquals(v, i & 0xf, "fromHex decode");
        }
    }

    @Test
    static void testFromHexInvalid() {
        for (int i = 0; i < 65536; i++) {
            char ch = (char)i;
            if (ch > 0xff || Character.digit(ch, 16) < 0) {
                assertFalse(HexFormat.isHexDigit(ch), "isHexDigit incorrect for '" + ch + "'  = " + i);
                expectThrows(NumberFormatException.class,
                        () -> HexFormat.fromHexDigit(ch));

            }
        }
    }

    @Test
    static void testAppendHexByteWithStringBuilder() {
        HexFormat hex = HexFormat.of();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 256; i++) {
            sb.setLength(0);
            StringBuilder sb1 = hex.toHexDigits(sb, (byte)i);
            assertSame(sb1, sb, "toHexDigits returned different StringBuilder");
            assertEquals(sb.length(), 2, "wrong length after append: " + i);
            assertEquals(sb.charAt(0), hex.toHighHexDigit((byte)i), "MSB converted wrong");
            assertEquals(sb.charAt(1), hex.toLowHexDigit((byte)i), "LSB converted wrong");

            assertEquals(HexFormat.fromHexDigits(sb), i, "hex.format(sb, byte) wrong");
        }
    }

    @Test
    static void testAppendHexByteWithCharBuffer() {
        HexFormat hex = HexFormat.of();
        CharBuffer cb = CharBuffer.allocate(256);
        for (int i = 1; i <= 128; i++) {
            CharBuffer cb1 = hex.toHexDigits(cb, (byte)i);
            assertTrue(cb1 == cb);
            assertEquals(cb.position(), i * 2);
        }
        assertEquals(cb.remaining(), 0);
    }

    @Test
    static void testAppendHexByteWithCharArrayWriter() {
        HexFormat hex = HexFormat.of();
        CharArrayWriter caw = new CharArrayWriter();
        for (int i = 1; i <= 128; i++) {
            CharArrayWriter caw1 = hex.toHexDigits(caw, (byte)i);
            assertTrue(caw1 == caw);
            assertEquals(caw.size(), i * 2);
        }
    }

    @Test
    static void testFromHexPairInvalid() {
        HexFormat hex = HexFormat.of();

        // An assortment of invalid characters
        String chars = "-0--0-";
        for (int i = 0; i < chars.length(); i += 2) {
            final int ndx = i;
            Throwable ex = expectThrows(NumberFormatException.class,
                    () -> HexFormat.fromHexDigits(chars.subSequence(ndx, ndx+2)));
            System.out.println(ex);
        }
    }

    @Test(dataProvider = "HexStringsThrowing")
    static void testToBytesThrowing(String value, String sep, String prefix, String suffix) {
        HexFormat hex = HexFormat.ofDelimiter(sep).withPrefix(prefix).withSuffix(suffix);
        Throwable ex = expectThrows(IllegalArgumentException.class,
                () -> {
                    byte[] v = hex.parseHex(value);
                    System.out.println("str: " + value + ", actual: " + v + ", bytes: " +
                                    Arrays.toString(v));
                });
        System.out.println("ex: " + ex);
    }

    @Test
    static void testFactoryNPE() {
        assertThrows(NPE, () -> HexFormat.ofDelimiter(null));
        assertThrows(NPE, () -> HexFormat.of().withDelimiter(null));
        assertThrows(NPE, () -> HexFormat.of().withPrefix(null));
        assertThrows(NPE, () -> HexFormat.of().withSuffix(null));
    }

    @Test
    static void testFormatHexNPE() {
        assertThrows(NPE, () -> HexFormat.of().formatHex(null));
        assertThrows(NPE, () -> HexFormat.of().formatHex(null, 0, 1));
        assertThrows(NPE, () -> HexFormat.of().formatHex(null, null));
        assertThrows(NPE,  () -> HexFormat.of().formatHex(null, null, 0, 0));
        StringBuilder sb = new StringBuilder();
        assertThrows(NPE, () -> HexFormat.of().formatHex(sb, null));
        assertThrows(NPE, () -> HexFormat.of().formatHex(sb, null, 0, 1));
    }

    @Test
    static void testParseHexNPE() {
        assertThrows(NPE, () -> HexFormat.of().parseHex(null));
        assertThrows(NPE, () -> HexFormat.of().parseHex((String)null, 0, 0));
        assertThrows(NPE, () -> HexFormat.of().parseHex((char[])null, 0, 0));
    }

    @Test
    static void testFromHexNPE() {
        assertThrows(NPE, () -> HexFormat.fromHexDigits(null));
        assertThrows(NPE, () -> HexFormat.fromHexDigits(null, 0, 0));
        assertThrows(NPE, () -> HexFormat.fromHexDigitsToLong(null));
        assertThrows(NPE, () -> HexFormat.fromHexDigitsToLong(null, 0, 0));
    }

    @Test
    static void testToHexDigitsNPE() {
        assertThrows(NPE, () -> HexFormat.of().toHexDigits(null, (byte)0));
    }

    @Test(dataProvider = "BadParseHexThrowing")
    static void badParseHex(String string, int offset, int length,
                            Class<? extends Throwable> exClass) {
        assertThrows(exClass,
                () -> HexFormat.of().parseHex(string, offset, length));
        char[] chars = string.toCharArray();
        assertThrows(exClass,
                () -> HexFormat.of().parseHex(chars, offset, length));
    }

    @Test(dataProvider = "BadFromHexDigitsThrowing")
    static void badFromHexDigits(String string, int fromIndex, int toIndex,
                           Class<? extends Throwable> exClass) {
        assertThrows(exClass,
                () -> HexFormat.fromHexDigits(string, fromIndex, toIndex));
        assertThrows(exClass,
                () -> HexFormat.fromHexDigitsToLong(string, fromIndex, toIndex));
    }

    // Verify IAE for strings that are too long for the target primitive type
    // or the number of requested digits is too large.
    @Test
    static void wrongNumberDigits() {
        assertThrows(IllegalArgumentException.class,
                () -> HexFormat.fromHexDigits("9876543210"));
        assertThrows(IllegalArgumentException.class,
                () -> HexFormat.fromHexDigits("9876543210", 0, 9));
        assertThrows(IllegalArgumentException.class,
                () -> HexFormat.fromHexDigitsToLong("98765432109876543210"));
        assertThrows(IllegalArgumentException.class,
                () -> HexFormat.fromHexDigitsToLong("98765432109876543210", 0, 17));
    }

    @Test(dataProvider="HexFormattersParsers")
    static void testFormatter(String delimiter, String prefix, String suffix,
                                   boolean uppercase,
                                   HexFormat hex) {
        byte[] expected = genBytes('A', 15);
        String res = hex.formatHex(expected);
        assertTrue(res.startsWith(prefix), "Prefix not found");
        assertTrue(res.endsWith(suffix), "Suffix not found");
        int expectedLen = expected.length * (2 + prefix.length() +
                delimiter.length() + suffix.length()) - delimiter.length();
        assertEquals(res.length(), expectedLen, "String length");

        if (expected.length > 1) {
            // check prefix and suffix is present for each hex pair
            for (int i = 0; i < expected.length; i++) {
                int valueChars = prefix.length() + 2 + suffix.length();
                int offset = i * (valueChars + delimiter.length());
                String value = res.substring(offset, offset + valueChars);
                assertTrue(value.startsWith(prefix), "wrong prefix");
                assertTrue(value.endsWith(suffix), "wrong suffix");

                // Check case of digits
                String cc = value.substring(prefix.length(), prefix.length() + 2);
                assertEquals(cc,
                        (uppercase) ? cc.toUpperCase(Locale.ROOT) : cc.toLowerCase(Locale.ROOT),
                        "Case mismatch");
                if (i < expected.length - 1 && !delimiter.isEmpty()) {
                    // Check the delimiter is present for each pair except the last
                    assertEquals(res.substring(offset + valueChars,
                            offset + valueChars + delimiter.length()), delimiter);
                }
            }
        }
    }

    @Test(dataProvider="HexFormattersParsers")
    static void testFormatHexString(String unused1, String unused2, String unused3,
                                   boolean unused4, HexFormat hex) {
        byte[] expected = genBytes('A', 15);
        String s = hex.formatHex(expected);
        System.out.println("    formatted: " + s);

        byte[] actual = hex.parseHex(s);
        System.out.println("    parsed as: " + Arrays.toString(actual));
        int mismatch = Arrays.mismatch(expected, actual);
        assertEquals(actual, expected, "format/parse cycle failed, mismatch: " + mismatch);
    }

    @Test(dataProvider="HexFormattersParsers")
    static void testParseHexStringRange(String delimiter, String prefix, String suffix,
                                   boolean unused4, HexFormat hex) {
        byte[] expected = genBytes('A', 15);
        String s = hex.formatHex(expected);

        // Parse values 2, 3, 4 from the generated string
        int low = 2;
        int high = 5;
        int stride = prefix.length() + 2 + suffix.length() + delimiter.length();
        System.out.println("    formatted subrange: " +
                s.substring(low * stride, high * stride - delimiter.length()));
        byte[] actual = hex.parseHex(s, low * stride,
                high * stride - delimiter.length());
        System.out.println("    parsed as: " + Arrays.toString(actual));

        assertEquals(actual.length, (high - low), "array length");
        int mismatch = Arrays.mismatch(expected, low, high, actual, 0, high - low);
        assertEquals(mismatch, -1, "format/parse cycle failed, mismatch: " + mismatch);
    }

    @Test(dataProvider="HexFormattersParsers")
    static void testParseHexEmptyString(String delimiter, String prefix, String suffix,
                                        boolean unused4, HexFormat hex) {
        byte[] actual = hex.parseHex("");
        assertEquals(actual.length, 0, "empty string parse");
        actual = hex.parseHex("abc", 0, 0);
        assertEquals(actual.length, 0, "empty string range parse");
        actual = hex.parseHex(new char[1], 0, 0);
        assertEquals(actual.length, 0, "empty char array subrange empty parse");
    }

        @Test(dataProvider="HexFormattersParsers")
    static void testFormatHexRangeString(String unused1, String unused2, String unused3,
                                   boolean unused4, HexFormat hex) {
        byte[] expected = genBytes('A', 15);
        int low = 1;
        int high = expected.length - 2;
        String s = hex.formatHex(expected, low, high);
        System.out.println("    formatted: " + s);

        byte[] actual = hex.parseHex(s);
        System.out.println("    parsed as: " + Arrays.toString(actual));
        int mismatch = Arrays.mismatch(expected, low, high, actual, 0, high - low);
        assertEquals(mismatch, -1, "format/parse cycle failed, mismatch: " + mismatch);
    }

    @Test(dataProvider="HexFormattersParsers")
    static void testFormatHexAppendable(String unused1, String unused2, String unused3,
                                     boolean unused4, HexFormat hex) {
        byte[] expected = genBytes('A', 15);
        StringBuilder sb = new StringBuilder();
        StringBuilder s = hex.formatHex(sb, expected);
        assertEquals(s, sb, "formatHex returned unknown StringBuilder");
        System.out.println("    formatted: " + s);

        byte[] actual = hex.parseHex(s.toString());
        System.out.println("    parsed as: " + Arrays.toString(actual));
        int mismatch = Arrays.mismatch(expected, actual);
        assertEquals(actual, expected, "format/parse cycle failed, mismatch: " + mismatch);
    }

    @Test(dataProvider="HexFormattersParsers")
    static void testFormatHexRangeAppendable(String unused1, String unused2, String unused3,
                                     boolean unused4, HexFormat hex) {
        byte[] expected = genBytes('A', 15);
        int low = 1;
        int high = expected.length - 2;
        StringBuilder sb = new StringBuilder();
        StringBuilder s = hex.formatHex(sb, expected, low, high);
        assertEquals(s, sb, "formatHex returned unknown StringBuilder");
        System.out.println("    formatted: " + s);

        byte[] actual = hex.parseHex(s.toString());
        System.out.println("    parsed as: " + Arrays.toString(actual));
        byte[] sub = Arrays.copyOfRange(expected, low, high);
        System.out.println("actual: " + Arrays.toString(actual));
        System.out.println("sub   : " + Arrays.toString(sub));
        int mismatch = Arrays.mismatch(expected, low, high, actual, 0, high - low);

        assertEquals(actual, sub, "format/parse cycle failed, mismatch: " + mismatch);
        assertEquals(mismatch, -1, "format/parse cycle failed, mismatch: " + mismatch);
    }

    @Test(dataProvider="HexFormattersParsers")
    static void testFormatHexCharArray(String unused1, String unused2, String unused3,
                                     boolean unused4, HexFormat hex) {
        byte[] expected = genBytes('A', 15);
        String s = hex.formatHex(expected);
        System.out.println("    formatted: " + s);

        char[] chars = s.toCharArray();
        byte[] actual = hex.parseHex(chars, 0, chars.length);
        System.out.println("    parsed as: " + Arrays.toString(actual));
        int mismatch = Arrays.mismatch(expected, actual);
        assertEquals(actual, expected, "format/parse cycle failed, mismatch: " + mismatch);
    }

    @Test(dataProvider="HexFormattersParsers")
    static void testFormatHexCharArrayIndexed(String delimiter, String prefix, String suffix,
                                              boolean unused4, HexFormat hex) {
        byte[] expected = genBytes('A', 15);
        String s = hex.formatHex(expected);
        System.out.println("    formatted: " + s);


        // Parse values 2, 3, 4 from the generated string
        int low = 2;
        int high = 5;
        int stride = prefix.length() + 2 + suffix.length() + delimiter.length();
        System.out.println("    formatted subrange: " +
                s.substring(low * stride, high * stride - delimiter.length()));
        char[] chars = s.toCharArray();
        byte[] actual = hex.parseHex(chars, low * stride,
                high * stride - delimiter.length());
        System.out.println("    parsed as: " + Arrays.toString(actual));

        assertEquals(actual.length, (high - low), "array length");
        int mismatch = Arrays.mismatch(expected, low, high, actual, 0, high - low);
        assertEquals(mismatch, -1, "format/parse cycle failed, mismatch: " + mismatch);
    }

    @Test(dataProvider="HexFormattersParsers")
    static void testFormatterToString(String delimiter, String prefix, String suffix,
                                    boolean uppercase,
                                    HexFormat hex) {
        String actual = String.format(
                "uppercase: %s, delimiter: \"%s\", prefix: \"%s\", suffix: \"%s\"",
                uppercase, escapeNL(delimiter), escapeNL(prefix), escapeNL(suffix));
        System.out.println("    hex: " + actual);
        assertEquals(actual, hex.toString(), "Formatter toString mismatch");
    }

    @Test(dataProvider="HexFormattersParsers")
    static void testFormatterParameterMethods(String delimiter, String prefix, String suffix,
                                    boolean uppercase,
                                    HexFormat hex) {
        assertEquals(hex.delimiter(), delimiter);
        assertEquals(hex.prefix(), prefix);
        assertEquals(hex.suffix(), suffix);
        assertEquals(hex.isUpperCase(), uppercase);
    }

    @Test(dataProvider="HexFormattersParsers")
    static void testFormatterTestEquals(String delimiter, String prefix, String suffix,
                                    boolean uppercase,
                                    HexFormat expected) {
        HexFormat actual = HexFormat.of()
                .withDelimiter(delimiter)
                .withPrefix(prefix)
                .withSuffix(suffix);
        actual = uppercase ? actual.withUpperCase() : actual.withLowerCase();

        assertEquals(actual.delimiter(), delimiter, "delimiter");
        assertEquals(actual.prefix(), prefix, "prefix");
        assertEquals(actual.suffix(), suffix, "suffix");
        assertEquals(actual.isUpperCase(), uppercase, "uppercase");
        assertTrue(actual.equals(expected), "equals method");
        assertEquals(actual.hashCode(), expected.hashCode(), "hashCode");

        assertTrue(actual.equals(actual));   // equals self
        assertFalse(actual.equals(null));    // never equals null
    }

    @Test(dataProvider="HexFormattersParsers")
    static void testZeroLength(String delimiter, String prefix, String suffix, boolean uppercase,
                                HexFormat hex) {
        // Test formatting of zero length byte arrays, should produce no output
        StringBuilder sb = new StringBuilder();
        assertEquals(hex.formatHex(new byte[0]), "", "Zero length");
        assertEquals(hex.formatHex(new byte[0], 0, 0), "", "Zero length");

        hex.formatHex(sb, new byte[0]);
        assertEquals(sb.length(), 0, "length should not change");
        hex.formatHex(sb, new byte[0], 0, 0);
        assertEquals(sb.length(), 0, "length should not change");

    }
    private static String escapeNL(String string) {
        return string.replace("\n", "\\n")
                .replace("\r", "\\r");
    }

    @Test
    static void testfromHexDigitsToInt() {
        HexFormat hex = HexFormat.of();

        String allHex = "76543210";
        final int orig = 0x76543210;
        for (int digits = 0; digits <= 8; digits++) {
            String s = hex.toHexDigits(orig, digits);
            long actual = HexFormat.fromHexDigits(s, 0, digits);
            System.out.printf("    digits: %2d, formatted: \"%s\", parsed as: 0x%08x%n",
                    digits, s, actual);
            assertEquals(s, allHex.substring(8 - digits, 8));
            long expected = (digits < 8) ? orig & ~(0xffffffff << (4 * digits)) : orig;
            assertEquals(actual, expected);
        }
    }

    @Test
    static void testfromHexDigitsToLong() {
        HexFormat hex = HexFormat.of();

        String allHex = "fedcba9876543210";
        final long orig = 0xfedcba9876543210L;
        for (int digits = 0; digits <= 16; digits++) {
            String s = hex.toHexDigits(orig, digits);
            long actual = HexFormat.fromHexDigitsToLong(s, 0, digits);
            System.out.printf("    digits: %2d, formatted: \"%s\", parsed as: 0x%016xL%n",
                    digits, s, actual);
            assertEquals(s, allHex.substring(16 - digits, 16));
            long expected = (digits < 16) ? orig & ~(0xffffffffffffffffL << (4 * digits)) : orig;
            assertEquals(actual, expected);
        }
    }

    @Test
    static void testToHexDigitsLong() {
        HexFormat hex = HexFormat.of();

        String allHex = "fedcba9876543210";
        final long expected = 0xfedcba9876543210L;
        String s = hex.toHexDigits(expected);
        long actual = HexFormat.fromHexDigitsToLong(s);
        System.out.printf("    formatted: \"%s\", parsed as: 0x%016xL%n", s, actual);
        assertEquals(s, allHex);
        assertEquals(actual, expected);
    }

    @Test(dataProvider="HexFormattersParsers")
    static void testIOException(String delimiter, String prefix, String suffix, boolean uppercase,
                               HexFormat hex) {
        Appendable throwingAppendable = new ThrowingAppendable();
        assertThrows(UncheckedIOException.class,
                () -> hex.formatHex(throwingAppendable, new byte[1]));
        assertThrows(UncheckedIOException.class,
                () -> hex.formatHex(throwingAppendable, new byte[1], 0, 1));
        assertThrows(UncheckedIOException.class,
                () -> hex.toHexDigits(throwingAppendable, (byte)1));
    }

    @Test(dataProvider="HexFormattersParsers")
    static void testOOME(String delimiter, String prefix, String suffix, boolean uppercase,
                         HexFormat hex) {
        // compute the size of byte array that will exceed the buffer
        long valueChars = prefix.length() + 2 + suffix.length();
        long stride = valueChars + delimiter.length();
        long max = Integer.MAX_VALUE & 0xFFFFFFFFL;
        long len = max / stride;
        long remainder = max - ((len - 1) * stride);
        if (remainder > valueChars) {
            len++;
            remainder -= valueChars;
        }
        try {
            byte[] bytes = new byte[(int) len];
            Throwable ex = expectThrows(OutOfMemoryError.class,
                    () -> hex.formatHex(bytes));
            System.out.println("ex: " + ex);
        } catch (OutOfMemoryError oome) {
            System.out.printf("OOME: total mem: %08x, free mem: %08x, max mem: %08x%n",
                    Runtime.getRuntime().totalMemory(),
                    Runtime.getRuntime().freeMemory(),
                    Runtime.getRuntime().maxMemory());
            throw new SkipException("Insufficient Memory to test OOME");
        }

    }

    /**
     * Example code from the HexFormat javadoc.
     * Showing simple usage of the API using "assert" to express the correct results
     * when shown in the javadoc.
     * The additional TestNG asserts verify the correctness of the same code.
     */
    @Test
    private static void samples() {
        {
            // Primitive formatting and parsing.
            HexFormat hex = HexFormat.of();

            byte b = 127;
            String byteStr = hex.toHexDigits(b);
            System.out.println("    " + byteStr);

            byte byteVal = (byte) HexFormat.fromHexDigits(byteStr);
            assert(byteStr.equals("7f"));
            assert(b == byteVal);
            assertTrue(byteStr.equals("7f"));
            assertTrue(b == byteVal);


            char c = 'A';
            String charStr = hex.toHexDigits(c);
            System.out.println("    " + charStr);
            int charVal = HexFormat.fromHexDigits(charStr);
            assert(c == charVal);
            assertTrue(c == charVal);

            int i = 12345;
            String intStr = hex.toHexDigits(i);
            System.out.println("    " + intStr);
            int intVal = HexFormat.fromHexDigits(intStr);
            assert(i == intVal);
            assertTrue(i == intVal);

            long l = Long.MAX_VALUE;
            String longStr = hex.toHexDigits(l, 16);
            long longVal = HexFormat.fromHexDigitsToLong(longStr, 0, 16);
            System.out.println("    " + longStr + ", " + longVal);
            assert(l == longVal);
            assertTrue(l == longVal);
        }

        {
            // RFC 4752 Fingerprint
            HexFormat formatFingerprint = HexFormat.ofDelimiter(":").withUpperCase();
            byte[] bytes = {0, 1, 2, 3, 124, 125, 126, 127};
            String str = formatFingerprint.formatHex(bytes);
            System.out.println("    Formatted: " + str);

            byte[] parsed = formatFingerprint.parseHex(str);
            System.out.println("    Parsed: " + Arrays.toString(parsed));
            assert(Arrays.equals(bytes, parsed));
            assertTrue(Arrays.equals(bytes, parsed));
        }

        {
            // Comma separated formatting
            HexFormat commaFormat = HexFormat.ofDelimiter(",");
            byte[] bytes = {0, 1, 2, 3, 124, 125, 126, 127};
            String str = commaFormat.formatHex(bytes);
            System.out.println("    Formatted: " + str);

            byte[] parsed = commaFormat.parseHex(str);
            System.out.println("    Parsed: " + Arrays.toString(parsed));
            assert(Arrays.equals(bytes, parsed));
            assertTrue(Arrays.equals(bytes, parsed));
        }
        {
            // Text formatting
            HexFormat commaFormat = HexFormat.ofDelimiter(", ").withPrefix("#");
            byte[] bytes = {0, 1, 2, 3, 124, 125, 126, 127};
            String str = commaFormat.formatHex(bytes);
            System.out.println("    Formatted: " + str);

            byte[] parsed = commaFormat.parseHex(str);
            System.out.println("    Parsed:    " + Arrays.toString(parsed));
            assert(Arrays.equals(bytes, parsed));
            assertTrue(Arrays.equals(bytes, parsed));
        }
    }

    /**
     * A test implementation of Appendable that throws IOException on all methods.
     */
    static class ThrowingAppendable implements Appendable {
        @Override
        public Appendable append(CharSequence csq) throws IOException {
            throw new IOException(".append(CharSequence) always throws");
        }

        @Override
        public Appendable append(CharSequence csq, int start, int end) throws IOException {
            throw new IOException(".append(CharSequence, start, end) always throws");
        }

        @Override
        public Appendable append(char c) throws IOException {
            throw new IOException(".append(char) always throws");
        }
    }
}