package com.fasterxml.jackson.databind.convert;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.concurrent.atomic.AtomicBoolean;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;

// Tests for "old" coercions (pre-2.12), with `MapperFeature.ALLOW_COERCION_OF_SCALARS`
public class CoerceJDKScalarsTest extends BaseMapTest
{
    static class BooleanPOJO {
        public Boolean value;
    }

    private final ObjectMapper COERCING_MAPPER = jsonMapperBuilder()
            .enable(MapperFeature.ALLOW_COERCION_OF_SCALARS)
            .build();

    private final ObjectMapper NOT_COERCING_MAPPER = jsonMapperBuilder()
            .disable(MapperFeature.ALLOW_COERCION_OF_SCALARS)
            .build();

    /*
    /**********************************************************
    /* Unit tests: coercion from empty String
    /**********************************************************
     */

    public void testNullValueFromEmpty() throws Exception
    {
        // wrappers accept `null` fine
        _verifyNullOkFromEmpty(Boolean.class, null);
        // but primitives require non-null
        _verifyNullOkFromEmpty(Boolean.TYPE, Boolean.FALSE);

        _verifyNullOkFromEmpty(Byte.class, null);
        _verifyNullOkFromEmpty(Byte.TYPE, Byte.valueOf((byte) 0));
        _verifyNullOkFromEmpty(Short.class, null);
        _verifyNullOkFromEmpty(Short.TYPE, Short.valueOf((short) 0));
        _verifyNullOkFromEmpty(Character.class, null);
        _verifyNullOkFromEmpty(Character.TYPE, Character.valueOf((char) 0));
        _verifyNullOkFromEmpty(Integer.class, null);
        _verifyNullOkFromEmpty(Integer.TYPE, Integer.valueOf(0));
        _verifyNullOkFromEmpty(Long.class, null);
        _verifyNullOkFromEmpty(Long.TYPE, Long.valueOf(0L));
        _verifyNullOkFromEmpty(Float.class, null);
        _verifyNullOkFromEmpty(Float.TYPE, Float.valueOf(0.0f));
        _verifyNullOkFromEmpty(Double.class, null);
        _verifyNullOkFromEmpty(Double.TYPE, Double.valueOf(0.0));

        _verifyNullOkFromEmpty(BigInteger.class, null);
        _verifyNullOkFromEmpty(BigDecimal.class, null);
        _verifyNullOkFromEmpty(AtomicBoolean.class, null);
    }

    private void _verifyNullOkFromEmpty(Class<?> type, Object exp) throws IOException
    {
        Object result = COERCING_MAPPER.readerFor(type)
                .with(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT)
                .readValue("\"\"");
        if (exp == null) {
            assertNull(result);
        } else {
            assertEquals(exp, result);
        }
    }

    public void testNullFailFromEmpty() throws Exception
    {
        _verifyNullFail(Boolean.class);
        _verifyNullFail(Boolean.TYPE);

        _verifyNullFail(Byte.class);
        _verifyNullFail(Byte.TYPE);
        _verifyNullFail(Short.class);
        _verifyNullFail(Short.TYPE);
        _verifyNullFail(Character.class);
        _verifyNullFail(Character.TYPE);
        _verifyNullFail(Integer.class);
        _verifyNullFail(Integer.TYPE);
        _verifyNullFail(Long.class);
        _verifyNullFail(Long.TYPE);
        _verifyNullFail(Float.class);
        _verifyNullFail(Float.TYPE);
        _verifyNullFail(Double.class);
        _verifyNullFail(Double.TYPE);

        _verifyNullFail(BigInteger.class);
        _verifyNullFail(BigDecimal.class);
        _verifyNullFail(AtomicBoolean.class);
    }

    private void _verifyNullFail(Class<?> type) throws IOException
    {
        try {
            NOT_COERCING_MAPPER.readerFor(type).readValue("\"\"");
            fail("Should have failed for "+type);
        } catch (MismatchedInputException e) {
            verifyException(e, "Cannot coerce empty String");
        }
    }

    /*
    /**********************************************************
    /* Unit tests: coercion from secondary representations
    /**********************************************************
     */

    public void testStringCoercionOkBoolean() throws Exception
    {
        // first successful coercions. Boolean has a ton...
        _verifyCoerceSuccess("1", Boolean.TYPE, Boolean.TRUE);
        _verifyCoerceSuccess("1", Boolean.class, Boolean.TRUE);
        _verifyCoerceSuccess(quote("true"), Boolean.TYPE, Boolean.TRUE);
        _verifyCoerceSuccess(quote("true"), Boolean.class, Boolean.TRUE);
        _verifyCoerceSuccess(quote("True"), Boolean.TYPE, Boolean.TRUE);
        _verifyCoerceSuccess(quote("True"), Boolean.class, Boolean.TRUE);
        _verifyCoerceSuccess(quote("TRUE"), Boolean.TYPE, Boolean.TRUE);
        _verifyCoerceSuccess(quote("TRUE"), Boolean.class, Boolean.TRUE);
        _verifyCoerceSuccess("0", Boolean.TYPE, Boolean.FALSE);
        _verifyCoerceSuccess("0", Boolean.class, Boolean.FALSE);
        _verifyCoerceSuccess(quote("false"), Boolean.TYPE, Boolean.FALSE);
        _verifyCoerceSuccess(quote("false"), Boolean.class, Boolean.FALSE);
        _verifyCoerceSuccess(quote("False"), Boolean.TYPE, Boolean.FALSE);
        _verifyCoerceSuccess(quote("False"), Boolean.class, Boolean.FALSE);
        _verifyCoerceSuccess(quote("FALSE"), Boolean.TYPE, Boolean.FALSE);
        _verifyCoerceSuccess(quote("FALSE"), Boolean.class, Boolean.FALSE);
    }

    public void testStringCoercionOkNumbers() throws Exception
    {
        _verifyCoerceSuccess(quote("123"), Byte.TYPE, Byte.valueOf((byte) 123));
        _verifyCoerceSuccess(quote("123"), Byte.class, Byte.valueOf((byte) 123));
        _verifyCoerceSuccess(quote("123"), Short.TYPE, Short.valueOf((short) 123));
        _verifyCoerceSuccess(quote("123"), Short.class, Short.valueOf((short) 123));
        _verifyCoerceSuccess(quote("123"), Integer.TYPE, Integer.valueOf(123));
        _verifyCoerceSuccess(quote("123"), Integer.class, Integer.valueOf(123));
        _verifyCoerceSuccess(quote("123"), Long.TYPE, Long.valueOf(123));
        _verifyCoerceSuccess(quote("123"), Long.class, Long.valueOf(123));
        _verifyCoerceSuccess(quote("123.5"), Float.TYPE, Float.valueOf(123.5f));
        _verifyCoerceSuccess(quote("123.5"), Float.class, Float.valueOf(123.5f));
        _verifyCoerceSuccess(quote("123.5"), Double.TYPE, Double.valueOf(123.5));
        _verifyCoerceSuccess(quote("123.5"), Double.class, Double.valueOf(123.5));

        _verifyCoerceSuccess(quote("123"), BigInteger.class, BigInteger.valueOf(123));
        _verifyCoerceSuccess(quote("123.0"), BigDecimal.class, new BigDecimal("123.0"));

        AtomicBoolean ab = COERCING_MAPPER.readValue(quote("true"), AtomicBoolean.class);
        assertNotNull(ab);
        assertTrue(ab.get());
    }

    public void testStringCoercionFailBoolean() throws Exception
    {
        _verifyRootStringCoerceFail("true", Boolean.TYPE);
        _verifyRootStringCoerceFail("true", Boolean.class);
        _verifyRootStringCoerceFail("True", Boolean.TYPE);
        _verifyRootStringCoerceFail("True", Boolean.class);
        _verifyRootStringCoerceFail("TRUE", Boolean.TYPE);
        _verifyRootStringCoerceFail("TRUE", Boolean.class);

        _verifyRootStringCoerceFail("false", Boolean.TYPE);
        _verifyRootStringCoerceFail("false", Boolean.class);
    }

    public void testStringCoercionFailInteger() throws Exception
    {
        _verifyRootStringCoerceFail("123", Byte.TYPE);
        _verifyRootStringCoerceFail("123", Byte.class);
        _verifyRootStringCoerceFail("123", Short.TYPE);
        _verifyRootStringCoerceFail("123", Short.class);
        _verifyRootStringCoerceFail("123", Integer.TYPE);
        _verifyRootStringCoerceFail("123", Integer.class);
        _verifyRootStringCoerceFail("123", Long.TYPE);
        _verifyRootStringCoerceFail("123", Long.class);
    }

    public void testStringCoercionFailFloat() throws Exception
    {
        _verifyRootStringCoerceFail("123.5", Float.TYPE);
        _verifyRootStringCoerceFail("123.5", Float.class);
        _verifyRootStringCoerceFail("123.5", Double.TYPE);
        _verifyRootStringCoerceFail("123.5", Double.class);

        _verifyRootStringCoerceFail("123", BigInteger.class);
        _verifyRootStringCoerceFail("123.0", BigDecimal.class);
    }

    // [databind#2635], [databind#2770]
    public void testToBooleanCoercionFailBytes() throws Exception
    {
        final String beanDoc = aposToQuotes("{'value':1}");
        _verifyBooleanCoerceFail("1", true, JsonToken.VALUE_NUMBER_INT, "1", Boolean.TYPE);
        _verifyBooleanCoerceFail("1", true, JsonToken.VALUE_NUMBER_INT, "1", Boolean.class);
        _verifyBooleanCoerceFail(beanDoc, true, JsonToken.VALUE_NUMBER_INT, "1", BooleanPOJO.class);

        _verifyBooleanCoerceFail("1.25", true, JsonToken.VALUE_NUMBER_FLOAT, "1.25", Boolean.TYPE);
        _verifyBooleanCoerceFail("1.25", true, JsonToken.VALUE_NUMBER_FLOAT, "1.25", Boolean.class);
    }

    // [databind#2635], [databind#2770]
    public void testToBooleanCoercionFailChars() throws Exception
    {
        final String beanDoc = aposToQuotes("{'value':1}");
        _verifyBooleanCoerceFail("1", false, JsonToken.VALUE_NUMBER_INT, "1", Boolean.TYPE);
        _verifyBooleanCoerceFail("1", false, JsonToken.VALUE_NUMBER_INT, "1", Boolean.class);
        _verifyBooleanCoerceFail(beanDoc, false, JsonToken.VALUE_NUMBER_INT, "1", BooleanPOJO.class);

        _verifyBooleanCoerceFail("1.25", false, JsonToken.VALUE_NUMBER_FLOAT, "1.25", Boolean.TYPE);
        _verifyBooleanCoerceFail("1.25", false, JsonToken.VALUE_NUMBER_FLOAT, "1.25", Boolean.class);
    }

    public void testMiscCoercionFail() throws Exception
    {
        // And then we have coercions from more esoteric types too

        _verifyCoerceFail("65", Character.class,
                "Cannot coerce Integer value (65) to `java.lang.Character` value");
        _verifyCoerceFail("65", Character.TYPE,
                "Cannot coerce Integer value (65) to `char` value");
    }

    /*
    /**********************************************************
    /* Helper methods
    /**********************************************************
     */

    private void _verifyCoerceSuccess(String input, Class<?> type, Object exp) throws IOException
    {
        Object result = COERCING_MAPPER.readerFor(type)
                .readValue(input);
        assertEquals(exp, result);
    }

    private void _verifyCoerceFail(String input, Class<?> type,
            String... expMatches) throws IOException
    {
        try {
            NOT_COERCING_MAPPER.readerFor(type)
                .readValue(input);
            fail("Should not have allowed coercion");
        } catch (MismatchedInputException e) {
            verifyException(e, expMatches);
        }
    }

    private void _verifyRootStringCoerceFail(String unquotedValue, Class<?> type) throws IOException
    {
        // Test failure for root value: for both byte- and char-backed sources:

        final String input = quote(unquotedValue);
        try (JsonParser p = NOT_COERCING_MAPPER.createParser(new StringReader(input))) {
            _verifyStringCoerceFail(p, unquotedValue, type);
        }
        final byte[] inputBytes = utf8Bytes(input);
        try (JsonParser p = NOT_COERCING_MAPPER.createParser(new ByteArrayInputStream(inputBytes))) {
            _verifyStringCoerceFail(p, unquotedValue, type);
        }
    }

    private void _verifyStringCoerceFail(JsonParser p,
            String unquotedValue, Class<?> type) throws IOException
    {
        try {
            NOT_COERCING_MAPPER.readerFor(type)
                .readValue(p);
            fail("Should not have allowed coercion");
        } catch (MismatchedInputException e) {
            verifyException(e, "Cannot coerce ");
            verifyException(e, " to `");
            verifyException(e, "` value");

            assertNotNull(e.getProcessor());
            assertSame(p, e.getProcessor());

            assertToken(JsonToken.VALUE_STRING, p.currentToken());
            assertEquals(unquotedValue, p.getText());
        }
    }

    private void _verifyBooleanCoerceFail(String doc, boolean useBytes,
            JsonToken tokenType, String tokenValue, Class<?> targetType) throws IOException
    {
        // Test failure for root value: for both byte- and char-backed sources.

        // [databind#2635]: important, need to use `readValue()` that takes content and NOT
        // JsonParser, as this forces closing of underlying parser and exposes more issues.

        final ObjectReader r = NOT_COERCING_MAPPER.readerFor(targetType);
        try {
            if (useBytes) {
                r.readValue(utf8Bytes(doc));
            } else {
                r.readValue(doc);
            }
            fail("Should not have allowed coercion");
        } catch (MismatchedInputException e) {
            _verifyBooleanCoerceFailReason(e, tokenType, tokenValue);
        }
    }

    @SuppressWarnings("resource")
    private void _verifyBooleanCoerceFailReason(MismatchedInputException e,
            JsonToken tokenType, String tokenValue) throws IOException
    {
        // 2 different possibilities here
        verifyException(e, "Cannot coerce Integer value", "Cannot deserialize value of type `");

        JsonParser p = (JsonParser) e.getProcessor();

        assertToken(tokenType, p.currentToken());

        final String text = p.getText();

        if (!tokenValue.equals(text)) {
            String textDesc = (text == null) ? "NULL" : quote(text);
            fail("Token text ("+textDesc+") via parser of type "+p.getClass().getName()
                    +" not as expected ("+quote(tokenValue)+"); exception message: '"+e.getMessage()+"'");
        }
    }
}
