/* ====================================================================
   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to You 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 org.apache.poi.ss.util;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.math.BigInteger;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

/**
 * Tests for {@link ExpandedDouble}
 */
final class TestExpandedDouble {
    private static final BigInteger BIG_POW_10 = BigInteger.valueOf(1000000000);

    @Test
    void testNegative() {
        ExpandedDouble hd = new ExpandedDouble(0xC010000000000000L);
        assertNotEquals(-2046, hd.getBinaryExponent(), "identified bug - sign bit not masked out of exponent");
        assertEquals(2, hd.getBinaryExponent());
        BigInteger frac = hd.getSignificand();
        assertEquals(64, frac.bitLength());
        assertEquals(1, frac.bitCount());
    }

    @Test
    void testSubnormal() {
        ExpandedDouble hd = new ExpandedDouble(0x0000000000000001L);
        assertNotEquals(-1023, hd.getBinaryExponent(), "identified bug - subnormal numbers not decoded properly");
        assertEquals(-1086, hd.getBinaryExponent());
        BigInteger frac = hd.getSignificand();
        assertEquals(64, frac.bitLength());
        assertEquals(1, frac.bitCount());
    }

    /**
     * Tests specific values for conversion from {@link ExpandedDouble} to {@link NormalisedDecimal} and back
     */
    @ParameterizedTest
    @ValueSource(longs = {
        0x4010000000000004L,
        0x7010000000000004L,
        0x1010000000000004L,
        0x0010000000000001L, // near lowest normal number
        0x0010000000000000L, // lowest normal number
        0x000FFFFFFFFFFFFFL, // highest subnormal number
        0x0008000000000000L, // subnormal number

        0xC010000000000004L,
        0xE230100010001004L,
        0x403CE0FFFFFFFFF2L,
        0x0000000000000001L, // smallest non-zero number (subnormal)
        0x6230100010000FFEL,
        0x6230100010000FFFL,
        0x6230100010001000L,
        0x403CE0FFFFFFFFF0L, // has single digit round trip error
        0x2B2BFFFF10001079L,
    })
    void confirmRoundTrip(long rawBitsA) {
        double a = Double.longBitsToDouble(rawBitsA);
        if (a == 0.0) {
            // Can't represent 0.0 or -0.0 with NormalisedDecimal
            return;
        }
        ExpandedDouble ed1 = new ExpandedDouble(rawBitsA);
        NormalisedDecimal nd2 = ed1.normaliseBaseTen();
        checkNormaliseBaseTenResult(ed1, nd2);

        ExpandedDouble ed3 = nd2.normaliseBaseTwo();
        assertEquals(ed3.getBinaryExponent(), ed1.getBinaryExponent(), "bin exp mismatch");

        BigInteger diff = ed3.getSignificand().subtract(ed1.getSignificand()).abs();
        if (diff.signum() == 0) {
            return;
        }
        // original quantity only has 53 bits of precision
        // these quantities may have errors in the 64th bit, which hopefully don't make any difference

        // errors in the 64th bit happen from time to time
        // this is well below the 53 bits of precision required
        assertTrue(diff.bitLength() < 2);
    }


    private static void checkNormaliseBaseTenResult(ExpandedDouble orig, NormalisedDecimal result) {
        String sigDigs = result.getSignificantDecimalDigits();
        BigInteger frac = orig.getSignificand();
        while (frac.bitLength() + orig.getBinaryExponent() < 200) {
            frac = frac.multiply(BIG_POW_10);
        }
        int binaryExp = orig.getBinaryExponent() - orig.getSignificand().bitLength();

        String origDigs = frac.shiftLeft(binaryExp+1).toString(10);
        assertTrue(origDigs.startsWith(sigDigs));

        double dO = Double.parseDouble("0." + origDigs.substring(sigDigs.length()));
        double d1 = Double.parseDouble(result.getFractionalPart().toPlainString());
        BigInteger subDigsO = BigInteger.valueOf((int) (dO * 32768 + 0.5));
        BigInteger subDigsB = BigInteger.valueOf((int) (d1 * 32768 + 0.5));

        if (subDigsO.equals(subDigsB)) {
            return;
        }
        BigInteger diff = subDigsB.subtract(subDigsO).abs();
        // 100/32768 ~= 0.003
        assertTrue(diff.intValue() <= 100, "minor mistake");
    }
}
