/*
 * 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.openjpa.util;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import org.apache.openjpa.kernel.FillStrategy;
import org.apache.openjpa.kernel.ResultShape;
import org.junit.Test;


import static org.junit.Assert.*;

public class TestResultShape {

    @Test
    public void testPrimitiveShapeIsImmutable() {
        ResultShape<Object> shape = new ResultShape<>(Object.class, true);
        assertCategory(shape, true, false, false);
        assertEquals(FillStrategy.Assign.class, shape.getStrategy().getClass());

        try {
            shape.add(int.class);
            fail(shape + " should not allow adding other shapes");
        } catch (UnsupportedOperationException e) {
        }
        try {
            shape.nest(Object[].class, new FillStrategy.Array(Object[].class), int.class, double.class);
            fail(shape + " should not allow nesting other shapes");
        } catch (UnsupportedOperationException e) {
        }
    }

    @Test
    public void testArrayIsMutable() {
        ResultShape<Object[]> shape = new ResultShape<>(Object[].class);
        assertCategory(shape, false, true, false);
        assertEquals(FillStrategy.Array.class, shape.getStrategy().getClass());

        shape.add(int.class, double.class); // will add primitive shapes
        assertCategory(shape, false, true, false);

        ResultShape<Object> primitiveShape = new ResultShape<>(Object.class, true);
        shape.nest(primitiveShape);
        assertCategory(shape, false, true, false);

        ResultShape<Object[]> nonPrimitiveShape = new ResultShape<>(Object[].class);
        nonPrimitiveShape.add(int.class, double.class);
        assertCategory(nonPrimitiveShape, false, true, false);
        shape.nest(nonPrimitiveShape);
        assertCategory(shape, false, true, true);
    }

    @Test
    public void testMethodImpliesMapStrategy() {
        FillStrategy<Map> strategy = new FillStrategy.Map<>(method(Map.class, "put", Object.class, Object.class));
        ResultShape<Map> mapShape = new ResultShape<>(Map.class, strategy, true);
        assertCategory(mapShape, true, false, false);
        assertEquals(FillStrategy.Map.class, mapShape.getStrategy().getClass());
    }

    @Test
    public void testShapeWithConstrcutorStrategy() {
        FillStrategy<List> strategy = new FillStrategy.NewInstance<>(constructor(ArrayList.class, int.class));
        ResultShape<List> listShape = new ResultShape<>(List.class, strategy);
        assertCategory(listShape, false, true, false);
        assertEquals(FillStrategy.NewInstance.class, listShape.getStrategy().getClass());
    }

    @Test
    public void testGetCompositeTypes() {
        ResultShape<Object[]> root  = new ResultShape<>(Object[].class);
        FillStrategy<Bar> strategy1 = new FillStrategy.NewInstance<>(Bar.class);
        ResultShape<Bar> bar1 = new ResultShape<>(Bar.class, strategy1, false);
        bar1.add(int.class);
        FillStrategy<Foo> strategy2 = new FillStrategy.NewInstance<>(constructor(Foo.class, short.class, Bar.class));
        ResultShape<Foo> fooBarConstructor = new ResultShape<>(Foo.class, strategy2);
        fooBarConstructor.add(short.class);
        fooBarConstructor.nest(bar1);
        root.add(Foo.class, Object.class);
        root.nest(fooBarConstructor);
        ResultShape<Bar> bar2 = new ResultShape<Bar>(Bar.class, new FillStrategy.NewInstance(Bar.class), false);
        root.nest(bar2);
        assertEquals("Object[]{Foo, Object, Foo{short, Bar{int}}, Bar}", root.toString());
        assertEquals(Arrays.asList(Foo.class, Object.class, short.class, int.class, Bar.class),
                root.getCompositeTypes());
        assertEquals(Arrays.asList(Foo.class, Object.class, Foo.class, Bar.class), root.getTypes());
        assertEquals(5, root.argLength());
        assertEquals(4, root.length());
    }

    @Test
    public void testRecursiveNestingIsNotAllowed() {
        ResultShape<Object[]> root = new ResultShape<>(Object[].class);
        ResultShape<Bar> bar1 = new ResultShape<Bar>(Bar.class, new FillStrategy.NewInstance(Bar.class), false);
        bar1.add(int.class);
        ResultShape<Foo> fooBarConstructor = new ResultShape<Foo>(Foo.class,
                new FillStrategy.NewInstance(constructor(Foo.class, short.class, Bar.class)));
        fooBarConstructor.add(short.class);
        fooBarConstructor.nest(bar1);
        root.add(Foo.class, Object.class);
        root.nest(fooBarConstructor);
        ResultShape<Bar> bar2 = new ResultShape<Bar>(Bar.class, new FillStrategy.NewInstance(Bar.class), false);
        root.nest(bar2);

        try {
            bar1.nest(fooBarConstructor);
            fail("Expecetd recursive nesting error in nest " + fooBarConstructor + " in " + bar1);
        } catch (IllegalArgumentException e) {

        }
    }


    @Test
    public void testFill() {
        //Fill this shape: Foo{short, Bar{String, Double}};
        ResultShape<Foo> foo = new ResultShape<Foo>(Foo.class, new FillStrategy.NewInstance(Foo.class), false);
        ResultShape<Bar> bar = new ResultShape<Bar>(Bar.class, new FillStrategy.NewInstance(Bar.class), false);
        bar.add(String.class, Double.class);
        foo.add(short.class);
        foo.nest(bar);
        assertEquals("Foo{short, Bar{String, Double}}", foo.toString());

        //from this array: 200s, "bar1", 12.3d)
        Object[] values = {(short)200, "bar1", 12.3d};
        Class[]  types  = {short.class, String.class, Double.class};
        String[]  aliases  = {"foo-short", "foo-bar-string", "foo-bar-Double"};
        Foo result = foo.pack(values, types, aliases);
        assertEquals(200, result.shrt);
        assertEquals("bar1", result.b.string);
        assertEquals(12.3, (double) result.b.Dbl, 0.1d);
    }

    @Test
    public void testFill2() {
        //Fill this shape: Object[]{Foo, Object, Foo{short, Bar{String, Double}}, Bar{double}};
        ResultShape<Object[]> root = new ResultShape<>(Object[].class);
        ResultShape<Bar> bar1 = new ResultShape<>(Bar.class, new FillStrategy.NewInstance<>(Bar.class));
        bar1.add(String.class, Double.class);
        ResultShape<Foo> fooBarConstr = new ResultShape<>(Foo.class, new FillStrategy.NewInstance<>(Foo.class));
        fooBarConstr.add(short.class);
        fooBarConstr.nest(bar1);
        ResultShape<Bar> bar2 = new ResultShape<>(Bar.class, new FillStrategy.NewInstance<>(Bar.class));
        bar2.add(double.class);
        root.add(Foo.class, Object.class);
        root.nest(fooBarConstr);
        root.nest(bar2);
        assertEquals("Object[]{Foo, Object, Foo{short, Bar{String, Double}}, Bar{double}}", root.toString());

        //from this array: new Foo(), new Object(), 200s, "bar1", 12.3d, 45.6d)
        Object[] values = {new Foo(), new Object(), 200, "bar1", 12.3d, 45.6d};
        Class[]  types  = {Foo.class, Object.class, short.class, String.class, Double.class, double.class};
        String[]  aliases  = {"Foo", "Object", "foo-short", "foo-bar-string", "foo-bar-Double", "bar-double"};
        Object[] result = root.pack(values, types, aliases);

        assertEquals(4, result.length);
        assertEquals(Foo.class, result[0].getClass());
        assertEquals(Object.class, result[1].getClass());
        assertEquals(Foo.class, result[2].getClass());
        assertEquals(Bar.class, result[3].getClass());
        assertEquals(200, ((Foo)result[2]).shrt);
        assertEquals("bar1", ((Foo)result[2]).b.string);
        assertEquals(12.3, (double)((Foo)result[2]).b.Dbl, 0.1d);
        assertEquals(45.6, ((Bar)result[3]).dbl, 0.1d);
    }

    void assertCategory(ResultShape<?> s, boolean primitive, boolean compound, boolean nesting) {
        if (primitive)
            assertTrue(s + " is not primitive", s.isPrimitive());
        else
            assertFalse(s + " is primitive", s.isPrimitive());
        if (compound)
            assertTrue(s + " is not compound", s.isCompound());
        else
            assertFalse(s + " is compound", s.isCompound());
        if (nesting)
            assertTrue(s + " is not nesting", s.isNesting());
        else
            assertFalse(s + " is nesting", s.isNesting());
    }

    void arrayEquals(Object[] a, Object[] b) {
        assertEquals(a.length, b.length);
        for (int i = 0; i < a.length; i++) {
            assertEquals(i+"-th element not equal", a[i], b[i]);
        }
    }

    <T> Constructor<T> constructor(Class<T> t, Class<?>...args) {
        try {
            return t.getConstructor(args);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    <T> Method method(Class<T> t, String name, Class<?>...args) {
        try {
            return t.getMethod(name, args);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static class Foo {
        private String string;
        private int i;
        private short shrt;
        private Bar b;
        public Foo() {}
        public Foo(String s, int i) {this.string = s; this.i = i;}
        public Foo(short s, Bar b){this.shrt = s; this.b = b;}
        @Override
        public String toString() {
            return "Foo(string='"+string+"' i="+i+" short="+shrt+" bar="+b+"";}
    }

    public static class Bar {
        private String string;
        private Double Dbl;
        private double dbl;
        public Bar() {}
        public Bar(double d) {this.dbl = d;}
        public Bar(String s, Double i) {this.string = s; this.Dbl = i;}
        @Override
        public String toString() {return "Bar(string='"+string+"' Dbl="+Dbl+" dbl="+dbl+"";}
    }
}
