/*
 * Copyright (c) 2025, 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.
 */

/*
 * @test
 * @bug 4068067 4101150 8366401
 * @library /java/text/testlib
 * @build HexDumpReader
 * @summary Check serialization of DecimalFormatSymbols. That is, ensure the
 *          behavior for each stream version is correct during de-serialization.
 * @run junit/othervm --add-opens java.base/java.text=ALL-UNNAMED DFSSerializationTest
 */

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.text.DecimalFormatSymbols;
import java.util.Currency;
import java.util.Locale;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class DFSSerializationTest {

    // Test that rely on hex dump files that were written from older JDK versions
    @Nested
    class HexDumpTests {

        @Test // See 4068067 and CDFS which is the class in the serialized hex dump
        void JDK1_1_4Test() {
            // Reconstruct a class serialized during 1.1.4 which has a DFS holder
            var cdfs = (CheckDecimalFormatSymbols) assertDoesNotThrow(
                    () -> deSer("DecimalFormatSymbols.114.txt"));
            assertDoesNotThrow(cdfs::Update); // Checks getDigit call succeeds
        }

        @Test // See 4068067
        void JDK1_4_2Test() {
            // Reconstruct a 1.4.2 DFS
            var dfs = (DecimalFormatSymbols) assertDoesNotThrow(
                    () -> deSer("DecimalFormatSymbols.142.txt"));
            // Checks curr symbol is saved, and exponent separator default set
            assertEquals("E", dfs.getExponentSeparator());
            assertEquals("*SpecialCurrencySymbol*", dfs.getCurrencySymbol());
        }
    }

    @Nested
    class StreamVersionTests {

        // Ensure correct monetarySeparator and exponential field defaults
        // Reads monetary from decimal, and sets exponential to 'E'
        @Test
        void version0Test() {
            var crafted = new DFSBuilder()
                    .setVer(0)
                    .set("monetarySeparator", '~')
                    .set("exponential", 'Z')
                    .build();
            var bytes = ser(crafted);
            var dfs = assertDoesNotThrow(() -> deSer(bytes));
            // Check exponential is set to proper default 'E', not 'Z'
            assertEquals('E', readField(dfs, "exponential"));
            // Ensure that mSep is based on dSep, and is not '~'
            assertNotEquals('~', dfs.getMonetaryDecimalSeparator());
            assertEquals(dfs.getDecimalSeparator(), dfs.getMonetaryDecimalSeparator());
        }

        // Version 1 did not have a locale field, and it defaulted to Locale.ROOT.
        // Note that other versions did allow a locale field, which was nullable.
        // E.g. see nullableLocaleTest which does not set locale when it is `null`
        @Test
        void version1Test() {
            var crafted = new DFSBuilder()
                    .setVer(1)
                    .set("locale", null)
                    .build();
            var bytes = ser(crafted);
            var dfs = assertDoesNotThrow(() -> deSer(bytes));
            assertEquals(Locale.ROOT, dfs.getLocale());
        }

        // Version 2 did not have an exponential separator, and created it via exponent
        // char field.
        @Test
        void version2Test() {
            var crafted = new DFSBuilder()
                    .setVer(2)
                    .set("exponentialSeparator", null)
                    .set("exponential", '~')
                    .build();
            var bytes = ser(crafted);
            var dfs = assertDoesNotThrow(() -> deSer(bytes));
            assertEquals("~", dfs.getExponentSeparator());
        }

        // Version 3 didn't have perMillText, percentText, and minusSignText.
        // These were created from the corresponding char equivalents.
        @Test
        void version3Test() {
            var crafted = new DFSBuilder()
                    .setVer(3)
                    .set("perMillText", null)
                    .set("percentText", null)
                    .set("minusSignText", null)
                    .set("perMill", '~')
                    .set("percent", '~')
                    .set("minusSign", '~')
                    .build();
            var bytes = ser(crafted);
            var dfs = assertDoesNotThrow(() -> deSer(bytes));
            // Need to check these String fields using reflection, since they
            // are not exposed via the public API
            assertEquals("~", readField(dfs, "perMillText"));
            assertEquals("~", readField(dfs, "percentText"));
            assertEquals("~", readField(dfs, "minusSignText"));
        }

        // Version 4 did not have monetaryGroupingSeparator. It should be based
        // off of groupingSeparator.
        @Test
        void version4Test() {
            var crafted = new DFSBuilder()
                    .setVer(4)
                    .set("monetaryGroupingSeparator", 'Z')
                    .set("groupingSeparator", '~')
                    .build();
            var bytes = ser(crafted);
            var dfs = assertDoesNotThrow(() -> deSer(bytes));
            assertEquals(dfs.getGroupingSeparator(), dfs.getMonetaryGroupingSeparator());
        }
    }

    // Up-to-date DFS stream versions do not expect a null locale since the
    // standard DecimalFormatSymbols API forbids it. However, this was not always
    // the case and previous stream versions can contain a null locale. Thus,
    // ensure that a null locale does not cause number data loading to fail.
    @Test
    void nullableLocaleTest() {
        var bytes = ser(new DFSBuilder()
                .set("locale", null)
                .set("minusSignText", "zFoo")
                .set("minusSign", 'z') // Set so that char/String forms agree
                .build());
        var dfs = assertDoesNotThrow(() -> deSer(bytes));
        assertNull(dfs.getLocale());
        // LMS should be based off of minusSignText when locale is null
        assertEquals("zFoo", readField(dfs, "lenientMinusSigns"));
    }

    // readObject fails when the {@code char} and {@code String} representations
    // of percent, per mille, and/or minus sign disagree.
    @Test
    void disagreeingTextTest() {
        var expected = "'char' and 'String' representations of either percent, " +
                "per mille, and/or minus sign disagree.";
        assertEquals(expected, assertThrows(InvalidObjectException.class, () ->
                deSer(ser(new DFSBuilder()
                        .set("minusSignText", "Z")
                        .set("minusSign", 'X')
                        .build()))).getMessage());
        assertEquals(expected, assertThrows(InvalidObjectException.class, () ->
                deSer(ser(new DFSBuilder()
                        .set("perMillText", "Z")
                        .set("perMill", 'X')
                        .build()))).getMessage());
        assertEquals(expected, assertThrows(InvalidObjectException.class, () ->
                deSer(ser(new DFSBuilder()
                        .set("percentText", "Z")
                        .set("percent", 'X')
                        .build()))).getMessage());
    }

    // Ensure the serial version is updated to the current after de-serialization.
    @Test
    void updatedVersionTest() {
        var bytes = ser(new DFSBuilder().setVer(-25).build());
        var dfs = assertDoesNotThrow(() -> deSer(bytes));
        assertEquals(5, readField(dfs, "serialVersionOnStream"));
    }

    // Should set currency from 4217 code when it is valid.
    @Test
    void validIntlCurrencyTest() {
        var bytes = ser(new DFSBuilder().set("intlCurrencySymbol", "JPY").build());
        var dfs = assertDoesNotThrow(() -> deSer(bytes));
        assertEquals(Currency.getInstance("JPY"), dfs.getCurrency());
    }

    // Should not set currency when 4217 code is invalid, it remains null.
    @Test
    void invalidIntlCurrencyTest() {
        var bytes = ser(new DFSBuilder()
                .set("intlCurrencySymbol", ">.,")
                .set("locale", Locale.JAPAN)
                .build());
        var dfs = assertDoesNotThrow(() -> deSer(bytes));
        // Can not init off invalid 4217 code, remains null
        assertNull(dfs.getCurrency());
    }

    // Ensure the currency symbol is read properly
    @Test
    void currencySymbolTest() {
        var crafted = new DecimalFormatSymbols();
        crafted.setCurrencySymbol("*SpecialCurrencySymbol*");
        var bytes = ser(crafted);
        var dfs = assertDoesNotThrow(() -> deSer(bytes));
        assertEquals("*SpecialCurrencySymbol*", dfs.getCurrencySymbol());
    }

    // Ensure the exponent separator is read properly
    @Test
    void exponentSeparatorTest() {
        var crafted = new DecimalFormatSymbols();
        crafted.setExponentSeparator("*SpecialExponentSeparator*");
        var bytes = ser(crafted);
        var dfs = assertDoesNotThrow(() -> deSer(bytes));
        assertEquals("*SpecialExponentSeparator*", dfs.getExponentSeparator());
    }

// Utilities ----

    // Utility to serialize
    private static byte[] ser(Object obj) {
        return assertDoesNotThrow(() -> {
            try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                 ObjectOutputStream oos = new ObjectOutputStream(byteArrayOutputStream)) {
                oos.writeObject(obj);
                return byteArrayOutputStream.toByteArray();
            }
        }, "Unexpected error during serialization");
    }

    // Utility to deserialize from byte array
    private static DecimalFormatSymbols deSer(byte[] bytes) throws IOException, ClassNotFoundException {
        try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
             ObjectInputStream ois = new ObjectInputStream(byteArrayInputStream)) {
            return (DecimalFormatSymbols) ois.readObject();
        }
    }

    // Utility to deserialize from file in hex format
    private static Object deSer(String file) throws IOException, ClassNotFoundException {
        try (InputStream stream = HexDumpReader.getStreamFromHexDump(file);
             ObjectInputStream ois = new ObjectInputStream(stream)) {
            return ois.readObject();
        }
    }

    // Utility to read a private field
    private static Object readField(DecimalFormatSymbols dfs, String name) {
        return assertDoesNotThrow(() -> {
            var field = DecimalFormatSymbols.class.getDeclaredField(name);
            field.setAccessible(true);
            return field.get(dfs);
        }, "Unexpected error during field reading");
    }

    // Utility class to build instances of DFS via reflection
    private static class DFSBuilder {

        private final DecimalFormatSymbols dfs;

        private DFSBuilder() {
            dfs = new DecimalFormatSymbols();
        }

        private DFSBuilder setVer(Object value) {
            return set("serialVersionOnStream", value);
        }

        private DFSBuilder set(String field, Object value) {
            return assertDoesNotThrow(() -> {
                Field f = dfs.getClass().getDeclaredField(field);
                f.setAccessible(true);
                f.set(dfs, value);
                return this;
            }, "Unexpected error during reflection setting");
        }

        private DecimalFormatSymbols build() {
            return dfs;
        }
    }
}

// Not nested, so that it can be cast correctly for the 1.1.4 test
class CheckDecimalFormatSymbols implements Serializable {
    DecimalFormatSymbols _decFormatSymbols = new DecimalFormatSymbols();
    public char Update()
    {
        return  _decFormatSymbols.getDigit();
    }
}
