/*
 * Copyright (C) 2011, 2012, 2013, 2014, 2015 XStream Committers.
 * All rights reserved.
 *
 * The software in this package is published under the terms of the BSD
 * style license a copy of which has been included with this distribution in
 * the LICENSE.txt file.
 *
 * Created on 29. July 2011 by Joerg Schaible
 */
package com.thoughtworks.acceptance;

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

import com.thoughtworks.acceptance.objects.StandardObject;
import com.thoughtworks.xstream.InitializationException;

/**
 * @author J&ouml;rg Schaible
 */
public class ImplicitArrayTest extends AbstractAcceptanceTest {

    protected void setUp() throws Exception {
        super.setUp();
        xstream.alias("farm", Farm.class);
        xstream.alias("animal", Animal.class);
        xstream.alias("MEGA-farm", MegaFarm.class);
        xstream.alias("area", Area.class);
        xstream.alias("country", Country.class);
        xstream.ignoreUnknownElements();
    }

    public static class Farm extends StandardObject {
        private transient int idx;
        Animal[] animals;
    }

    public static class Animal extends StandardObject implements Comparable {
        String name;

        public Animal(String name) {
            this.name = name;
        }

        public int compareTo(Object o) {
            return name.compareTo(((Animal)o).name);
        }
    }
    
    public void testWithDirectType() {
        Farm farm = new Farm();
        farm.animals = new Animal[] {
            new Animal("Cow"),
            new Animal("Sheep")
        };

        String expected = "" +
                "<farm>\n" +
                "  <animal>\n" +
                "    <name>Cow</name>\n" +
                "  </animal>\n" +
                "  <animal>\n" +
                "    <name>Sheep</name>\n" +
                "  </animal>\n" +
                "</farm>";

        xstream.addImplicitArray(Farm.class, "animals");
        assertBothWays(farm, expected);
    }

    public static class MegaFarm extends Farm {
        String separator = "---";
        String[] names;
    }

    public void testInheritsImplicitArrayFromSuperclass() {
        Farm farm = new MegaFarm(); // subclass
        farm.animals = new Animal[] {
            new Animal("Cow"),
            new Animal("Sheep")
        };

        String expected = "" +
                "<MEGA-farm>\n" +
                "  <animal>\n" +
                "    <name>Cow</name>\n" +
                "  </animal>\n" +
                "  <animal>\n" +
                "    <name>Sheep</name>\n" +
                "  </animal>\n" +
                "  <separator>---</separator>\n" +
                "</MEGA-farm>";

        xstream.addImplicitCollection(Farm.class, "animals");
        assertBothWays(farm, expected);
    }

    public void testSupportsInheritedAndDirectDeclaredImplicitArraysAtOnce() {
        MegaFarm farm = new MegaFarm(); // subclass
        farm.animals = new Animal[] {
            new Animal("Cow"),
            new Animal("Sheep")
        };
        farm.names = new String[] {
            "McDonald",
            "Ponte Rosa"
        };
        
        String expected = "" +
                "<MEGA-farm>\n" +
                "  <animal>\n" +
                "    <name>Cow</name>\n" +
                "  </animal>\n" +
                "  <animal>\n" +
                "    <name>Sheep</name>\n" +
                "  </animal>\n" +
                "  <separator>---</separator>\n" +
                "  <name>McDonald</name>\n" +
                "  <name>Ponte Rosa</name>\n" +
                "</MEGA-farm>";

        xstream.addImplicitArray(Farm.class, "animals");
        xstream.addImplicitArray(MegaFarm.class, "names", "name");
        assertBothWays(farm, expected);
    }

    public void testInheritedAndDirectDeclaredImplicitArraysAtOnceIsNotDeclarationSequenceDependent() {
        MegaFarm farm = new MegaFarm(); // subclass
        farm.animals = new Animal[] {
            new Animal("Cow"),
            new Animal("Sheep")
        };
        farm.names = new String[] {
            "McDonald",
            "Ponte Rosa"
        };
        
        String expected = "" +
                "<MEGA-farm>\n" +
                "  <animal>\n" +
                "    <name>Cow</name>\n" +
                "  </animal>\n" +
                "  <animal>\n" +
                "    <name>Sheep</name>\n" +
                "  </animal>\n" +
                "  <separator>---</separator>\n" +
                "  <name>McDonald</name>\n" +
                "  <name>Ponte Rosa</name>\n" +
                "</MEGA-farm>";

        xstream.addImplicitArray(MegaFarm.class, "names", "name");
        xstream.addImplicitArray(Farm.class, "animals");
        assertBothWays(farm, expected);
    }

    public void testAllowsSubclassToOverrideImplicitCollectionInSuperclass() {
        Farm farm = new MegaFarm(); // subclass
        farm.animals = new Animal[] {
            new Animal("Cow"),
            new Animal("Sheep")
        };

        String expected = "" +
                "<MEGA-farm>\n" +
                "  <animal>\n" +
                "    <name>Cow</name>\n" +
                "  </animal>\n" +
                "  <animal>\n" +
                "    <name>Sheep</name>\n" +
                "  </animal>\n" +
                "  <separator>---</separator>\n" +
                "</MEGA-farm>";

        xstream.addImplicitCollection(MegaFarm.class, "animals");
        assertBothWays(farm, expected);
    }

    public void testAllowDifferentImplicitArrayDefinitionsInSubclass() {
        Farm farm = new Farm();
        farm.animals = new Animal[] {
           new Animal("Cod"), 
           new Animal("Salmon") 
        };
        MegaFarm megaFarm = new MegaFarm(); // subclass
        megaFarm.animals = new Animal[] {
            new Animal("Cow"),
            new Animal("Sheep")
        };
        megaFarm.names = new String[] {
            "McDonald",
            "Ponte Rosa"
        };
        
        List list = new ArrayList();
        list.add(farm);
        list.add(megaFarm);
        String expected = "" +
                "<list>\n" +
                "  <farm>\n" +
                "    <fish>\n" +
                "      <name>Cod</name>\n" +
                "    </fish>\n" +
                "    <fish>\n" +
                "      <name>Salmon</name>\n" +
                "    </fish>\n" +
                "  </farm>\n" +
                "  <MEGA-farm>\n" +
                "    <animal>\n" +
                "      <name>Cow</name>\n" +
                "    </animal>\n" +
                "    <animal>\n" +
                "      <name>Sheep</name>\n" +
                "    </animal>\n" +
                "    <separator>---</separator>\n" +
                "    <name>McDonald</name>\n" +
                "    <name>Ponte Rosa</name>\n" +
                "  </MEGA-farm>\n" +
                "</list>";

        xstream.addImplicitArray(Farm.class, "animals", "fish");
        xstream.addImplicitArray(MegaFarm.class, "animals");
        xstream.addImplicitArray(MegaFarm.class, "names", "name");
        assertBothWays(list, expected);
    }

    public static class House extends StandardObject {
        private Room[] rooms;
        private String separator = "---";
        private Person[] people;
        
        public List getPeople() {
            return Arrays.asList(people);
        }
        
        public List getRooms() {
            return Arrays.asList(rooms);
        }
    }

    public static class Room extends StandardObject {
        private String name;

        public Room(String name) {
            this.name = name;
        }
    }

    public static class Person extends StandardObject {
        private String name;
        private String[] emailAddresses;

        public Person(String name) {
            this.name = name;
        }
    }

    public void testMultipleArraysBasedOnDifferentType() {
        House house = new House();
        house.rooms = new Room[] {
            new Room("kitchen"),
            new Room("bathroom")
        };
        Person joe = new Person("joe");
        joe.emailAddresses = new String[]{
            "joe@house.org",
            "joe.farmer@house.org"
        };
        Person jaimie = new Person("jaimie");
        jaimie.emailAddresses = new String[]{
            "jaimie@house.org",
            "jaimie.farmer@house.org",
            "jaimie.ann.farmer@house.org"
        };
        house.people = new Person[]{
            joe,
            jaimie
        };

        String expected = ""
                + "<house>\n"
                + "  <room>\n"
                + "    <name>kitchen</name>\n"
                + "  </room>\n"
                + "  <room>\n"
                + "    <name>bathroom</name>\n"
                + "  </room>\n"
                + "  <separator>---</separator>\n"
                + "  <person>\n"
                + "    <name>joe</name>\n"
                + "    <email>joe@house.org</email>\n"
                + "    <email>joe.farmer@house.org</email>\n"
                + "  </person>\n"
                + "  <person>\n"
                + "    <name>jaimie</name>\n"
                + "    <email>jaimie@house.org</email>\n"
                + "    <email>jaimie.farmer@house.org</email>\n"
                + "    <email>jaimie.ann.farmer@house.org</email>\n"
                + "  </person>\n"
                + "</house>";

        xstream.alias("room", Room.class);
        xstream.alias("house", House.class);
        xstream.alias("person", Person.class);
        xstream.addImplicitArray(House.class, "rooms");
        xstream.addImplicitArray(House.class, "people");
        xstream.addImplicitArray(Person.class, "emailAddresses", "email");

        House serializedHouse = (House)assertBothWays(house, expected);
        assertEquals(house.getPeople(), serializedHouse.getPeople());
        assertEquals(house.getRooms(), serializedHouse.getRooms());
    }

    public static class NumberedRoom extends Room {
        private int number;

        public NumberedRoom(int number) {
            super("room");
            this.number = number;
        }
    }

    public void testArraysWithDerivedElements() {
        House house = new House();
        house.rooms = new Room[] {
            new Room("kitchen"),
            new NumberedRoom(13)
        };

        String expected = ""
                + "<house>\n"
                + "  <room>\n"
                + "    <name>kitchen</name>\n"
                + "  </room>\n"
                + "  <room class=\"chamber\" number=\"13\">\n"
                + "    <name>room</name>\n"
                + "  </room>\n"
                + "  <separator>---</separator>\n"
                + "</house>";

        xstream.alias("house", House.class);
        xstream.alias("chamber", NumberedRoom.class);
        xstream.addImplicitArray(House.class, "rooms", "room");
        xstream.useAttributeFor(int.class);

        House serializedHouse = (House)assertBothWays(house, expected);
        assertEquals(house.getRooms(), serializedHouse.getRooms());
    }

    public static class Aquarium extends StandardObject {
        private String name;
        private String[] fish;

        public Aquarium(String name) {
            this.name = name;
        }
    }

    public void testWithExplicitItemNameMatchingTheNameOfTheFieldWithTheArray() {
        Aquarium aquarium = new Aquarium("hatchery");
        aquarium.fish = new String[]{
            "salmon",
            "halibut",
            "snapper"
        };

        String expected = "" +
                "<aquarium>\n" +
                "  <name>hatchery</name>\n" +
                "  <fish>salmon</fish>\n" +
                "  <fish>halibut</fish>\n" +
                "  <fish>snapper</fish>\n" +
                "</aquarium>";

        xstream.alias("aquarium", Aquarium.class);
        xstream.addImplicitArray(Aquarium.class, "fish", "fish");

        assertBothWays(aquarium, expected);
    }
    
    public void testWithImplicitNameMatchingTheNameOfTheFieldWithTheArray() {
        Aquarium aquarium = new Aquarium("hatchery");
        aquarium.fish = new String[]{
            "salmon",
            "halibut",
            "snapper"
        };

        String expected = "" +
                "<aquarium>\n" +
                "  <name>hatchery</name>\n" +
                "  <fish>salmon</fish>\n" +
                "  <fish>halibut</fish>\n" +
                "  <fish>snapper</fish>\n" +
                "</aquarium>";

        xstream.alias("aquarium", Aquarium.class);
        xstream.alias("fish", String.class);
        xstream.addImplicitArray(Aquarium.class, "fish");

        assertBothWays(aquarium, expected);
    }
    
    public void testWithAliasedItemNameMatchingTheAliasedNameOfTheFieldWithTheArray() {
        Aquarium aquarium = new Aquarium("hatchery");
        aquarium.fish = new String[]{
            "salmon",
            "halibut",
            "snapper"
        };

        String expected = "" +
                "<aquarium>\n" +
                "  <name>hatchery</name>\n" +
                "  <animal>salmon</animal>\n" +
                "  <animal>halibut</animal>\n" +
                "  <animal>snapper</animal>\n" +
                "</aquarium>";

        xstream.alias("aquarium", Aquarium.class);
        xstream.aliasField("animal", Aquarium.class, "fish");
        xstream.addImplicitArray(Aquarium.class, "fish", "animal");

        assertBothWays(aquarium, expected);
    }

    public void testCanBeDeclaredOnlyForMatchingType() {
        try {
            xstream.addImplicitArray(Animal.class, "name");
            fail("Thrown " + InitializationException.class.getName() + " expected");
        } catch (final InitializationException e) {
            assertTrue(e.getMessage().indexOf("declares no collection") >= 0);
        }
    }

    public void testCanBeDeclaredOnlyForMatchingComponentType() {
        try {
            xstream.addImplicitArray(Aquarium.class, "fish", Farm.class);
            fail("Thrown " + InitializationException.class.getName() + " expected");
        } catch (final InitializationException e) {
            assertTrue(e.getMessage().indexOf("array type is not compatible") >= 0);
        }
    }

    public void testWithNullElement() {
        Farm farm = new Farm();
        farm.animals = new Animal[] {
            new Animal("Cow"),
            null,
            new Animal("Sheep")
        };

        String expected = "" +
                "<farm>\n" +
                "  <animal>\n" +
                "    <name>Cow</name>\n" +
                "  </animal>\n" +
                "  <null/>\n" +
                "  <animal>\n" +
                "    <name>Sheep</name>\n" +
                "  </animal>\n" +
                "</farm>";

        xstream.addImplicitArray(Farm.class, "animals");
        assertBothWays(farm, expected);
    }

    public void testWithAliasAndNullElement() {
        Farm farm = new Farm();
        farm.animals = new Animal[] {
            null,
            new Animal("Sheep")
        };

        String expected = "" +
                "<farm>\n" +
                "  <null/>\n" +
                "  <beast>\n" +
                "    <name>Sheep</name>\n" +
                "  </beast>\n" +
                "</farm>";

        xstream.addImplicitArray(Farm.class, "animals", "beast");
        assertBothWays(farm, expected);
    }
    
    public static class Area extends Farm {
        Animal[] animals;
    }
    
    public void testWithHiddenArray() {
        Area area = new Area();
        ((Farm)area).animals = new Animal[2];
        ((Farm)area).animals[0] = new Animal("Cow");
        ((Farm)area).animals[1] = new Animal("Sheep");
        area.animals = new Animal[2];
        area.animals[0] = new Animal("Falcon");
        area.animals[1] = new Animal("Sparrow");

        String expected = "" +
                "<area>\n" +
                "  <animal defined-in=\"farm\">\n" +
                "    <name>Cow</name>\n" +
                "  </animal>\n" +
                "  <animal defined-in=\"farm\">\n" +
                "    <name>Sheep</name>\n" +
                "  </animal>\n" +
                "  <animal>\n" +
                "    <name>Falcon</name>\n" +
                "  </animal>\n" +
                "  <animal>\n" +
                "    <name>Sparrow</name>\n" +
                "  </animal>\n" +
                "</area>";

        xstream.addImplicitArray(Farm.class, "animals");
        xstream.addImplicitArray(Area.class, "animals");
        assertBothWays(area, expected);
    }
    
    public void testWithHiddenArrayAndDifferentAlias() {
        Area area = new Area();
        ((Farm)area).animals = new Animal[2];
        ((Farm)area).animals[0] = new Animal("Cow");
        ((Farm)area).animals[1] = new Animal("Sheep");
        area.animals = new Animal[2];
        area.animals[0] = new Animal("Falcon");
        area.animals[1] = new Animal("Sparrow");

        String expected = "" +
                "<area>\n" +
                "  <domesticated defined-in=\"farm\">\n" +
                "    <name>Cow</name>\n" +
                "  </domesticated>\n" +
                "  <domesticated defined-in=\"farm\">\n" +
                "    <name>Sheep</name>\n" +
                "  </domesticated>\n" +
                "  <wild>\n" +
                "    <name>Falcon</name>\n" +
                "  </wild>\n" +
                "  <wild>\n" +
                "    <name>Sparrow</name>\n" +
                "  </wild>\n" +
                "</area>";

        xstream.addImplicitArray(Farm.class, "animals", "domesticated");
        xstream.addImplicitArray(Area.class, "animals", "wild");
        assertBothWays(area, expected);
    }
    
    public void testDoesNotInheritFromHiddenArrayOfSuperclass() {
        Area area = new Area();
        ((Farm)area).animals = new Animal[2];
        ((Farm)area).animals[0] = new Animal("Cow");
        ((Farm)area).animals[1] = new Animal("Sheep");
        area.animals = new Animal[2];
        area.animals[0] = new Animal("Falcon");
        area.animals[1] = new Animal("Sparrow");

        String expected = "" +
                "<area>\n" +
                "  <animal defined-in=\"farm\">\n" +
                "    <name>Cow</name>\n" +
                "  </animal>\n" +
                "  <animal defined-in=\"farm\">\n" +
                "    <name>Sheep</name>\n" +
                "  </animal>\n" +
                "  <animals>\n" +
                "    <animal>\n" +
                "      <name>Falcon</name>\n" +
                "    </animal>\n" +
                "    <animal>\n" +
                "      <name>Sparrow</name>\n" +
                "    </animal>\n" +
                "  </animals>\n" +
                "</area>";

        xstream.addImplicitArray(Farm.class, "animals");
        assertBothWays(area, expected);
    }
    
    public void testDoesNotPropagateToHiddenArrayOfSuperclass() {
        Area area = new Area();
        ((Farm)area).animals = new Animal[2];
        ((Farm)area).animals[0] = new Animal("Cow");
        ((Farm)area).animals[1] = new Animal("Sheep");
        area.animals = new Animal[2];
        area.animals[0] = new Animal("Falcon");
        area.animals[1] = new Animal("Sparrow");

        String expected = "" +
                "<area>\n" +
                "  <animals defined-in=\"farm\">\n" +
                "    <animal>\n" +
                "      <name>Cow</name>\n" +
                "    </animal>\n" +
                "    <animal>\n" +
                "      <name>Sheep</name>\n" +
                "    </animal>\n" +
                "  </animals>\n" +
                "  <animal>\n" +
                "    <name>Falcon</name>\n" +
                "  </animal>\n" +
                "  <animal>\n" +
                "    <name>Sparrow</name>\n" +
                "  </animal>\n" +
                "</area>";

        xstream.addImplicitArray(Area.class, "animals");
        assertBothWays(area, expected);
    }
    
    public static class County extends Area {
    }
    
    public static class Country extends County {
        Animal[] animals;
    }
    
    public void testWithDoubleHiddenArray() {
        Country country = new Country();
        ((Farm)country).animals = new Animal[2];
        ((Farm)country).animals[0] = new Animal("Cow");
        ((Farm)country).animals[1] = new Animal("Sheep");
        ((Area)country).animals = new Animal[2];
        ((Area)country).animals[0] = new Animal("Falcon");
        ((Area)country).animals[1] = new Animal("Sparrow");
        country.animals = new Animal[2];
        country.animals[0] = new Animal("Wale");
        country.animals[1] = new Animal("Dolphin");

        String expected = "" +
                "<country>\n" +
                "  <animal defined-in=\"farm\">\n" +
                "    <name>Cow</name>\n" +
                "  </animal>\n" +
                "  <animal defined-in=\"farm\">\n" +
                "    <name>Sheep</name>\n" +
                "  </animal>\n" +
                "  <animal defined-in=\"area\">\n" +
                "    <name>Falcon</name>\n" +
                "  </animal>\n" +
                "  <animal defined-in=\"area\">\n" +
                "    <name>Sparrow</name>\n" +
                "  </animal>\n" +
                "  <animal>\n" +
                "    <name>Wale</name>\n" +
                "  </animal>\n" +
                "  <animal>\n" +
                "    <name>Dolphin</name>\n" +
                "  </animal>\n" +
                "</country>";

        xstream.addImplicitArray(Farm.class, "animals");
        xstream.addImplicitArray(Area.class, "animals");
        xstream.addImplicitArray(Country.class, "animals");
        assertBothWays(country, expected);
    }
    
    static class PrimitiveArray {
        int[] ints; 
    };

    public void testWithPrimitiveArray() {
        PrimitiveArray pa = new PrimitiveArray();
        pa.ints = new int[]{ 47, 11 };

        String expected = "" +
                "<primitives>\n" +
                "  <int>47</int>\n" +
                "  <int>11</int>\n" +
                "</primitives>";

        xstream.alias("primitives", PrimitiveArray.class);
        xstream.addImplicitArray(PrimitiveArray.class, "ints");
        assertBothWays(pa, expected);
    }

    static class MultiDimenstionalArrays {
        Object[][] multiObject; 
        String[][] multiString; 
        int[][] multiInt; 
    };

    public void testMultiDimensionalDirectArray() {
        MultiDimenstionalArrays multiDim = new MultiDimenstionalArrays();
        multiDim.multiString = new String[][]{ 
            new String[]{ "1", "2" }, 
            new String[]{ "a", "b", "c" }
        };

        String expected = "" +
                "<N>\n" +
                "  <string-array>\n" +
                "    <string>1</string>\n" +
                "    <string>2</string>\n" +
                "  </string-array>\n" +
                "  <string-array>\n" +
                "    <string>a</string>\n" +
                "    <string>b</string>\n" +
                "    <string>c</string>\n" +
                "  </string-array>\n" +
                "</N>";

        xstream.alias("N", MultiDimenstionalArrays.class);
        xstream.addImplicitArray(MultiDimenstionalArrays.class, "multiString");
        assertBothWays(multiDim, expected);
    }

    public void testMultiDimensionalPrimitiveArray() {
        MultiDimenstionalArrays multiDim = new MultiDimenstionalArrays();
        multiDim.multiInt = new int[][]{ 
            new int[]{ 1, 2 }, 
            new int[]{ 0, -1, -2 }
        };

        String expected = "" +
                "<N>\n" +
                "  <int-array>\n" +
                "    <int>1</int>\n" +
                "    <int>2</int>\n" +
                "  </int-array>\n" +
                "  <int-array>\n" +
                "    <int>0</int>\n" +
                "    <int>-1</int>\n" +
                "    <int>-2</int>\n" +
                "  </int-array>\n" +
                "</N>";

        xstream.alias("N", MultiDimenstionalArrays.class);
        xstream.addImplicitArray(MultiDimenstionalArrays.class, "multiInt");
        assertBothWays(multiDim, expected);
    }

    public void testMultiDimensionalArrayWithAlias() {
        MultiDimenstionalArrays multiDim = new MultiDimenstionalArrays();
        multiDim.multiObject = new Object[][]{ 
            new String[]{ "1" }, 
            new Object[]{ "a", Boolean.FALSE }
        };

        String expected = "" +
                "<N>\n" +
                "  <M class=\"string-array\">\n" +
                "    <string>1</string>\n" +
                "  </M>\n" +
                "  <M>\n" +
                "    <string>a</string>\n" +
                "    <boolean>false</boolean>\n" +
                "  </M>\n" +
                "</N>";

        xstream.alias("N", MultiDimenstionalArrays.class);
        xstream.addImplicitArray(MultiDimenstionalArrays.class, "multiObject", "M");
        assertBothWays(multiDim, expected);
    }
}
