/**
 * 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.cxf.aegis.type.encoded;

import java.math.BigDecimal;
import java.util.Arrays;

import javax.xml.namespace.QName;

import org.apache.cxf.aegis.Context;
import org.apache.cxf.aegis.type.AegisType;
import org.apache.cxf.aegis.type.basic.BeanTypeInfo;
import org.apache.cxf.aegis.xml.stax.ElementReader;

import org.junit.Before;
import org.junit.Test;

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

public class SoapArrayTypeTest extends AbstractEncodedTest {
    private static final String[][][] ARRAY_2_3_4 = new String[][][] {
        new String[][]{
            new String[]{"row1 col1 dep1", "row1 col1 dep2", "row1 col1 dep3", "row1 col1 dep4"},
            new String[]{"row1 col2 dep1", "row1 col2 dep2", "row1 col2 dep3", "row1 col2 dep4"},
            new String[]{"row1 col3 dep1", "row1 col3 dep2", "row1 col3 dep3", "row1 col3 dep4"},
        },
        new String[][]{
            new String[]{"row2 col1 dep1", "row2 col1 dep2", "row2 col1 dep3", "row2 col1 dep4"},
            new String[]{"row2 col2 dep1", "row2 col2 dep2", "row2 col2 dep3", "row2 col2 dep4"},
            new String[]{"row2 col3 dep1", "row2 col3 dep2", "row2 col3 dep3", "row2 col3 dep4"},
        },
    };

    @Before
    public void setUp() throws Exception {
        super.setUp();

        // address type
        BeanTypeInfo addressInfo = new BeanTypeInfo(Address.class, "urn:Bean");
        addressInfo.setTypeMapping(mapping);

        StructType addressType = new StructType(addressInfo);
        addressType.setTypeClass(Address.class);
        addressType.setSchemaType(new QName("urn:Bean", "address"));
        mapping.register(addressType);

        // purchase order type
        BeanTypeInfo poInfo = new BeanTypeInfo(PurchaseOrder.class, "urn:Bean");
        poInfo.setTypeMapping(mapping);

        StructType purchaseOrderType = new StructType(poInfo);
        purchaseOrderType.setTypeClass(PurchaseOrder.class);
        purchaseOrderType.setTypeMapping(mapping);
        purchaseOrderType.setSchemaType(new QName("urn:Bean", "po"));
        mapping.register(purchaseOrderType);

        // String[][][]
        SoapArrayType arrayOfString =
                createArrayType(String[].class, new QName("urn:Bean", "SOAPArrayOfString"));
        mapping.register(arrayOfString);
        SoapArrayType arrayOfArrayOfString =
                createArrayType(String[][].class, new QName("urn:Bean", "SOAPArrayOfArrayOfString"));
        mapping.register(arrayOfArrayOfString);
        SoapArrayType arrayOfArrayOfArrayOfString =
                createArrayType(String[][][].class, new QName("urn:Bean", "SOAPArrayOfArrayOfArrayOfString"));
        mapping.register(arrayOfArrayOfArrayOfString);

        // int[][]
        SoapArrayType arrayOfInt =
                createArrayType(int[].class, new QName("urn:Bean", "SOAPArrayOfInt"));
        mapping.register(arrayOfInt);
        SoapArrayType arrayOfArrayOfInt =
                createArrayType(int[][].class, new QName("urn:Bean", "SOAPArrayOfArrayOfInt"));
        mapping.register(arrayOfArrayOfInt);

        // Object[]
        SoapArrayType arrayOfAddress =
                createArrayType(Address[].class, new QName("urn:Bean", "SOAPArrayOfAddress"));
        mapping.register(arrayOfAddress);
        SoapArrayType arrayOfAny =
                createArrayType(Object[].class, new QName("urn:Bean", "SOAPArrayOfAny"));
        mapping.register(arrayOfAny);
    }

    @Test
    public void testSimpleArray() throws Exception {
        Context context = getContext();

        // xsd:int[2]
        ElementReader reader = new ElementReader(getClass().getResourceAsStream("arraySimple.xml"));
        int[] numbers = (int[]) createArrayType(int[].class).readObject(reader, context);
        reader.getXMLStreamReader().close();
        assertArrayEquals(new int[]{3, 4}, numbers);

        // round trip tests
        numbers = readWriteReadRef("arraySimple.xml", int[].class);
        assertArrayEquals(new int[]{3, 4}, numbers);
    }

    @Test
    public void testUrTypeArray() throws Exception {
        Context context = getContext();

        // ur-type[4] nested elements have xsi:type
        ElementReader reader = new ElementReader(getClass().getResourceAsStream("arrayUrType1.xml"));
        Object[] objects = (Object[]) createArrayType(Object[].class).readObject(reader, context);
        reader.getXMLStreamReader().close();
        assertArrayEquals(new Object[]{42, (float)42.42, "Forty Two"}, objects);

        // ur-type[4] nested element name have a global schema type
        reader = new ElementReader(getClass().getResourceAsStream("arrayUrType2.xml"));
        objects = (Object[]) createArrayType(Object[].class).readObject(reader, context);
        reader.getXMLStreamReader().close();
        assertArrayEquals(Arrays.asList(objects).toString(),
                          new Object[]{42, new BigDecimal("42.42"), "Forty Two"},
                          objects);
    }

    @Test
    public void testUrTypeArrayReadWriteRef1() throws Exception {
        Object[] objects;
        // round trip tests
        objects = readWriteReadRef("arrayUrType1.xml", Object[].class);
        assertArrayEquals(new Object[]{42, Float.valueOf(42.42f), "Forty Two"}, objects);
    }

    @Test
    public void testUrTypeArrayReadWriteRef2() throws Exception {
        Object[] objects;
        // round trip tests
        objects = readWriteReadRef("arrayUrType2.xml", Object[].class);
        assertArrayEquals(new Object[]{42, new BigDecimal("42.42"), "Forty Two"}, objects);

    }

    @Test
    public void testAnyTypeArray() throws Exception {
        Context context = getContext();

        // ur-type[4] nested elements have xsi:type
        ElementReader reader = new ElementReader(getClass().getResourceAsStream("arrayAnyType1.xml"));
        Object[] objects = (Object[]) createArrayType(Object[].class).readObject(reader, context);
        reader.getXMLStreamReader().close();
        assertArrayEquals(new Object[]{42, (float)42.42, "Forty Two"}, objects);

        // ur-type[4] nested element name have a global schema type
        reader = new ElementReader(getClass().getResourceAsStream("arrayAnyType2.xml"));
        objects = (Object[]) createArrayType(Object[].class).readObject(reader, context);
        reader.getXMLStreamReader().close();
        assertArrayEquals(new Object[]{42, new BigDecimal("42.42"), "Forty Two"}, objects);

        // round trip tests
        objects = readWriteReadRef("arrayAnyType1.xml", Object[].class);
        assertArrayEquals(new Object[]{42, (float)42.42, "Forty Two"}, objects);
        objects = readWriteReadRef("arrayAnyType2.xml", Object[].class);
        assertArrayEquals(new Object[]{42, new BigDecimal("42.42"), "Forty Two"}, objects);
    }

    @Test
    public void testStructArray() throws Exception {
        Context context = getContext();

        // b:address[2]
        ElementReader reader = new ElementReader(getClass().getResourceAsStream("arrayStructs.xml"));
        Address[] addresses = (Address[]) createArrayType(Address[].class).readObject(reader, context);
        reader.getXMLStreamReader().close();
        AbstractEncodedTest.validateShippingAddress(addresses[0]);
        AbstractEncodedTest.validateBillingAddress(addresses[1]);

        // round trip tests
        addresses = readWriteReadRef("arrayStructs.xml", Address[].class);
        AbstractEncodedTest.validateShippingAddress(addresses[0]);
        AbstractEncodedTest.validateBillingAddress(addresses[1]);
    }

    @Test
    public void testSquareArray() throws Exception {
        Context context = getContext();

        // xsd:string[2,3,4]
        ElementReader reader = new ElementReader(getClass().getResourceAsStream("arraySquare.xml"));
        String[][][] strings = (String[][][]) createArrayType(String[][][].class).readObject(reader, context);
        reader.getXMLStreamReader().close();
        assertArrayEquals(ARRAY_2_3_4, strings);

        // round trip tests
        strings = readWriteReadRef("arraySquare.xml", String[][][].class);
        assertArrayEquals(ARRAY_2_3_4, strings);
    }

    @Test
    public void testArrayOfArrays() throws Exception {
        Context context = getContext();

        // xsd:string[,][2]
        ElementReader reader = new ElementReader(getClass().getResourceAsStream("arrayArrayOfArrays1.xml"));
        String[][][] strings = (String[][][]) createArrayType(String[][][].class).readObject(reader, context);
        reader.getXMLStreamReader().close();
        assertArrayEquals(ARRAY_2_3_4, strings);

        // round trip tests
        strings = readWriteReadRef("arrayArrayOfArrays1.xml", String[][][].class);
        assertArrayEquals(ARRAY_2_3_4, strings);
    }

    @Test
    public void testPartiallyTransmitted() throws Exception {
        Context context = getContext();

        // xsd:int[5] offset="[2]"
        ElementReader reader = new ElementReader(
                getClass().getResourceAsStream("arrayPartiallyTransmitted.xml"));
        int[] numbers = (int[]) createArrayType(int[].class).readObject(reader, context);
        reader.getXMLStreamReader().close();
        assertArrayEquals(new int[]{0, 0, 3, 4, 0}, numbers);

        // round trip tests
        numbers = readWriteReadRef("arrayPartiallyTransmitted.xml", int[].class);
        assertArrayEquals(new int[]{0, 0, 3, 4, 0}, numbers);
    }

    @Test
    public void testSparseArray() throws Exception {
        Context context = getContext();

        // xsd:string[2,3,4]
        ElementReader reader = new ElementReader(getClass().getResourceAsStream("arraySparse1.xml"));
        String[][][] strings = (String[][][]) createArrayType(String[][][].class).readObject(reader, context);
        reader.getXMLStreamReader().close();
        verifySparseArray(strings);

        // xsd:string[,][4] -> xsd:string[3,4]
        reader = new ElementReader(getClass().getResourceAsStream("arraySparse2.xml"));
        strings = (String[][][]) createArrayType(String[][][].class).readObject(reader, context);
        reader.getXMLStreamReader().close();
        verifySparseArray(strings);

        // xsd:string[,][4] -> xsd:string[][3] -> xsd:string[4]
        reader = new ElementReader(getClass().getResourceAsStream("arraySparse3.xml"));
        strings = (String[][][]) createArrayType(String[][][].class).readObject(reader, context);
        reader.getXMLStreamReader().close();
        verifySparseArray(strings);

        // round trip tests
        strings = readWriteReadRef("arraySparse1.xml", String[][][].class);
        verifySparseArray(strings);
        strings = readWriteReadRef("arraySparse2.xml", String[][][].class);
        verifySparseArray(strings);
        strings = readWriteReadRef("arraySparse3.xml", String[][][].class);
        verifySparseArray(strings);
    }

    @Test
    public void testInvalidArray() throws Exception {
        // to many elements
        verifyInvalid("arrayInvalid1.xml", int[].class);
        // position out of bounds
        verifyInvalid("arrayInvalid2.xml", int[].class);
        // array dimensions mismatch
        verifyInvalid("arrayInvalid3.xml", int[].class);
        verifyInvalid("arrayInvalid4.xml", int[][].class);
        // array offset to large
        verifyInvalid("arrayInvalid5.xml", int[].class);
        verifyInvalid("arrayInvalid6.xml", int[].class);
        // duplicate entry in sparse array
        verifyInvalid("arrayInvalid7.xml", String[][][].class);
        // position doesn't have enough positions
        verifyInvalid("arrayInvalid8.xml", String[][][].class);
        // position has too many positions
        verifyInvalid("arrayInvalid9.xml", String[][][].class);
    }

    private void verifySparseArray(String[][][] strings) {
        assertEquals("row1 col1 dep1", strings[0][0][0]);
        strings[0][0][0] = null;
        assertEquals("row2 col1 dep1", strings[1][0][0]);
        strings[1][0][0] = null;
        assertEquals("row1 col2 dep1", strings[0][1][0]);
        strings[0][1][0] = null;
        assertEquals("row1 col1 dep2", strings[0][0][1]);
        strings[0][0][1] = null;

        assertEquals("row1 col3 dep1", strings[0][2][0]);
        strings[0][2][0] = null;
        assertEquals("row1 col1 dep4", strings[0][0][3]);
        strings[0][0][3] = null;

        assertEquals("row2 col3 dep4", strings[1][2][3]);
        strings[1][2][3] = null;

        assertEquals("row2 col3 dep2", strings[1][2][1]);
        strings[1][2][1] = null;
        assertEquals("row1 col2 dep3", strings[0][1][2]);
        strings[0][1][2] = null;

        assertEquals(2, strings.length);
        for (int i = 0; i < strings.length; i++) {
            if (strings[i] != null) {
                assertEquals(3, strings[i].length);
                for (int j = 0; j < strings[i].length; j++) {
                    if (strings[i][j] != null) {
                        assertEquals(4, strings[i][j].length);
                        for (int k = 0; k < strings[i][j].length; k++) {
                            assertNull("strings[" + i + "][" + j + "][" + k + "] is not null",
                                    strings[i][j][k]);
                        }
                    }
                }
            }
        }
    }

    private SoapArrayType createArrayType(Class<?> typeClass) {
        return createArrayType(typeClass, new QName("urn:Bean", "stuff"));
    }

    private SoapArrayType createArrayType(Class<?> typeClass, QName schemaType) {
        AegisType type = mapping.getType(typeClass);
        if (type != null) {
            return (SoapArrayType) type;
        }
        SoapArrayType arrayType = new SoapArrayType();
        arrayType.setTypeClass(typeClass);
        arrayType.setTypeMapping(mapping);
        arrayType.setSchemaType(schemaType);
        return arrayType;
    }
}