/**
 * Copyright (c) 2008, http://www.snakeyaml.org
 *
 * Licensed 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.yaml.snakeyaml.array;

import java.util.Arrays;
import java.util.List;

import junit.framework.TestCase;

import org.junit.Assert;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.ConstructorException;
import org.yaml.snakeyaml.error.YAMLException;

public class PrimitiveArrayTest extends TestCase {

    private final String pkg = "!!org.yaml.snakeyaml.array";

    private final byte[] bytes = new byte[] { 1, 2, 3 };
    private final short[] shorts = new short[] { 300, 301, 302 };
    private final int[] ints = new int[] { 40000, 40001, 40002 };
    private final long[] longs = new long[] { 5000000000L, 5000000001L };
    private final float[] floats = new float[] { 0.1f, 3.1415f };
    private final double[] doubles = new double[] { 50.0001, 2150.0002 };
    private final char[] chars = new char[] { 'a', 'b', 'c', 'd', 'e' };
    private final boolean[] bools = new boolean[] { true, false };

    public void testValidConstructor() {
        String testInput = "- " + pkg + ".ByteArr [ " + Arrays.toString(bytes) + " ]\n" + "- "
                + pkg + ".ShortArr [ " + Arrays.toString(shorts) + " ]\n" + "- " + pkg
                + ".IntArr [ " + Arrays.toString(ints) + " ]\n" + "- " + pkg + ".LongArr [ "
                + Arrays.toString(longs) + " ]\n" + "- " + pkg + ".FloatArr [ "
                + Arrays.toString(floats) + " ]\n" + "- " + pkg + ".DoubleArr [ "
                + Arrays.toString(doubles) + " ]\n" + "- " + pkg + ".CharArr [ "
                + Arrays.toString(chars) + " ]\n" + "- " + pkg + ".BooleanArr [ "
                + Arrays.toString(bools) + " ]\n";

        Yaml yaml = new Yaml();
        List<Object> wrappers = (List<Object>) yaml.load(testInput);

        Assert.assertArrayEquals(bytes, ((ByteArr) wrappers.get(0)).getBytes());
        Assert.assertArrayEquals(shorts, ((ShortArr) wrappers.get(1)).getShorts());
        Assert.assertArrayEquals(ints, ((IntArr) wrappers.get(2)).getInts());
        Assert.assertArrayEquals(longs, ((LongArr) wrappers.get(3)).getLongs());
        Assert.assertArrayEquals(floats, ((FloatArr) wrappers.get(4)).getFloats(), 0.001f);
        Assert.assertArrayEquals(doubles, ((DoubleArr) wrappers.get(5)).getDoubles(), 0.001);
        Assert.assertArrayEquals(chars, ((CharArr) wrappers.get(6)).getChars());
        assertArrayEquals(bools, ((BooleanArr) wrappers.get(7)).getBools());
    }

    /*
     * For some reason, every other assertArrayEquals specialization is provided
     * by org.junit.Assert, but not this one.
     */
    private void assertArrayEquals(boolean[] expected, boolean[] actuals) {
        assertEquals("Arrays differ in length", expected.length, actuals.length);
        for (int i = 0; i < expected.length; ++i) {
            if (expected[i] != actuals[i]) {
                fail("Arrays first differ at " + i + "; expected " + expected[i] + " but got "
                        + actuals[i]);
            }
        }
    }

    private void tryInvalid(String t, Class<?> expectedException) {
        Yaml yaml = new Yaml();
        try {
            Object loaded = yaml.load(t);
            fail("Expected " + expectedException.getCanonicalName() + " but loaded = \"" + loaded
                    + "\"");
        } catch (YAMLException e) {
            assertEquals(expectedException, e.getCause().getClass());
        }
    }

    public void testInvalidConstructors() {
        // Loading a character as any primitive other than 'char' is a
        // NumberFormatException
        tryInvalid(pkg + ".ByteArr [ [ 'a' ] ]", NumberFormatException.class);
        tryInvalid(pkg + ".ShortArr [ [ 'a' ] ]", NumberFormatException.class);
        tryInvalid(pkg + ".IntArr [ [ 'a' ] ]", NumberFormatException.class);
        tryInvalid(pkg + ".LongArr [ [ 'a' ] ]", NumberFormatException.class);
        tryInvalid(pkg + ".FloatArr [ [ 'a' ] ]", NumberFormatException.class);
        tryInvalid(pkg + ".DoubleArr [ [ 'a' ] ]", NumberFormatException.class);

        // Exception: because of how boolean construction works, constructing a
        // boolean from 'a'
        // results in null.
        tryInvalid(pkg + ".BooleanArr [ [ 'a' ] ]", NullPointerException.class);

        // Loading a floating-point number as a character is a YAMLException
        tryInvalid(pkg + ".CharArr [ [ 1.2 ] ]", YAMLException.class);

        // Loading a String as a Character is a YAMLException
        tryInvalid(pkg + ".CharArr [ [ 'abcd' ] ]", YAMLException.class);

    }

    public void testTruncation() {
        // Loading floating-point numbers as integer types is disallowed,
        // because that's a number-format problem.
        tryInvalid(pkg + ".ByteArr [ [ 3.14 ] ]", NumberFormatException.class);
        tryInvalid(pkg + ".ShortArr [ [ 3.14 ] ]", NumberFormatException.class);
        tryInvalid(pkg + ".IntArr [ [ 3.14 ] ]", NumberFormatException.class);
        tryInvalid(pkg + ".LongArr [ [ 3.14 ] ]", NumberFormatException.class);
    }

    public void testPromotion() {
        Yaml yaml = new Yaml();

        // Loading integer numbers as floating-point types is allowed...
        Assert.assertArrayEquals(new float[] { 3, 5 },
                ((FloatArr) yaml.load(pkg + ".FloatArr [ [ 3, 5 ] ] ")).getFloats(), 0.001f);

        Assert.assertArrayEquals(new double[] { 3, 5 },
                ((DoubleArr) yaml.load(pkg + ".DoubleArr [ [ 3, 5 ] ] ")).getDoubles(), 0.001f);
    }

    public void testStringCharArray() {
        Yaml yaml = new Yaml();

        try {
            yaml.load(pkg + ".CharArr [ [ abcd ] ]");
            fail("Expected exception.");
        } catch (Exception e) {
            assertEquals(ConstructorException.class, e.getClass());
            assertEquals("Invalid node Character: 'abcd'; length: 4", e.getCause().getMessage());
        }
    }

    private static Object cycle(Object in) {
        Yaml yaml = new Yaml();
        String dumped = yaml.dump(in);
        // System.out.println ( dumped );
        return yaml.load(dumped);
    }

    /**
     * All kinds of primitive arrays should be able to cycle from (Java array)
     * to (YAML string) to (Java array) again, and they should be exactly the
     * same before and after.
     */
    public void testCycle() {
        ByteArr byteArr = new ByteArr(bytes);
        Assert.assertArrayEquals(byteArr.getBytes(), ((ByteArr) cycle(byteArr)).getBytes());

        ShortArr shortArr = new ShortArr(shorts);
        Assert.assertArrayEquals(shortArr.getShorts(), ((ShortArr) cycle(shortArr)).getShorts());

        IntArr intArr = new IntArr(ints);
        Assert.assertArrayEquals(intArr.getInts(), ((IntArr) cycle(intArr)).getInts());

        LongArr longArr = new LongArr(longs);
        Assert.assertArrayEquals(longArr.getLongs(), ((LongArr) cycle(longArr)).getLongs());

        FloatArr floatArr = new FloatArr(floats);
        Assert.assertArrayEquals(floatArr.getFloats(), ((FloatArr) cycle(floatArr)).getFloats(),
                0.001f);

        DoubleArr doubleArr = new DoubleArr(doubles);
        Assert.assertArrayEquals(doubleArr.getDoubles(),
                ((DoubleArr) cycle(doubleArr)).getDoubles(), 0.001);

        CharArr charArr = new CharArr(chars);
        Assert.assertArrayEquals(charArr.getChars(), ((CharArr) cycle(charArr)).getChars());

        BooleanArr boolArr = new BooleanArr(bools);
        assertArrayEquals(boolArr.getBools(), ((BooleanArr) cycle(boolArr)).getBools());
    }

    public void testMultiDimensional() {
        Array2D two = new Array2D();
        two.setLongs(new long[][] { { 1, 2, 3 }, { 4, 5, 6 } });
        assertTrue(Arrays.deepEquals(two.getLongs(), ((Array2D) cycle(two)).getLongs()));

        Array3D three = new Array3D();
        three.setLongs(new long[][][] { { { 1, 2, 3, 4 }, { 5, 6, 7, 8 } },
                { { 9, 10, 11, 12 }, { 13, 14, 15, 16 } } });
        assertTrue(Arrays.deepEquals(three.getLongs(), ((Array3D) cycle(three)).getLongs()));

        // Object with an array of Objects which each have an array of
        // primitives.
        ArrayLongArr four = new ArrayLongArr();
        four.setContents(new LongArr[] { new LongArr(new long[] { 1, 2, 3, 4 }),
                new LongArr(new long[] { 5, 6, 7, 8 }) });
        Object result = cycle(four);
        assertEquals(four, (ArrayLongArr) result);
    }

    public static class Array2D {
        private long[][] longs;

        public long[][] getLongs() {
            return longs;
        }

        public void setLongs(long[][] longs) {
            this.longs = longs;
        }
    }

    public static class Array3D {
        private long[][][] longs;

        public long[][][] getLongs() {
            return longs;
        }

        public void setLongs(long[][][] longs) {
            this.longs = longs;
        }
    }

    public static class ArrayLongArr {
        private LongArr[] contents;

        public LongArr[] getContents() {
            return contents;
        }

        public void setContents(LongArr[] contents) {
            this.contents = contents;
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof ArrayLongArr))
                return false;

            ArrayLongArr other = ((ArrayLongArr) obj);
            if (contents.length != other.getContents().length)
                return false;
            for (int i = 0; i < contents.length; ++i) {
                if (!Arrays.equals(contents[i].getLongs(), other.getContents()[i].getLongs())) {
                    return false;
                }
            }

            return true;
        }
    }
}
