<?php

namespace Doctrine\Tests\ORM\Functional;

/**
 * @group DDC-93
 */
class ValueObjectsTest extends \Doctrine\Tests\OrmFunctionalTestCase
{
    public function setUp()
    {
        parent::setUp();

        try {
            $this->_schemaTool->createSchema(array(
                $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC93Person'),
                $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC93Address'),
                $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC93Vehicle'),
                $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC93Car'),
                $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC3027Animal'),
                $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC3027Dog'),
            ));
        } catch(\Exception $e) {
        }
    }

    public function testCRUD()
    {
        $person = new DDC93Person();
        $person->name = "Tara";
        $person->address = new DDC93Address();
        $person->address->street = "United States of Tara Street";
        $person->address->zip = "12345";
        $person->address->city = "funkytown";
        $person->address->country = new DDC93Country('Germany');

        // 1. check saving value objects works
        $this->_em->persist($person);
        $this->_em->flush();

        $this->_em->clear();

        // 2. check loading value objects works
        $person = $this->_em->find(DDC93Person::CLASSNAME, $person->id);

        $this->assertInstanceOf(DDC93Address::CLASSNAME, $person->address);
        $this->assertEquals('United States of Tara Street', $person->address->street);
        $this->assertEquals('12345', $person->address->zip);
        $this->assertEquals('funkytown', $person->address->city);
        $this->assertInstanceOf(DDC93Country::CLASSNAME, $person->address->country);
        $this->assertEquals('Germany', $person->address->country->name);

        // 3. check changing value objects works
        $person->address->street = "Street";
        $person->address->zip = "54321";
        $person->address->city = "another town";
        $person->address->country->name = "United States of America";
        $this->_em->flush();

        $this->_em->clear();

        $person = $this->_em->find(DDC93Person::CLASSNAME, $person->id);

        $this->assertEquals('Street', $person->address->street);
        $this->assertEquals('54321', $person->address->zip);
        $this->assertEquals('another town', $person->address->city);
        $this->assertEquals('United States of America', $person->address->country->name);

        // 4. check deleting works
        $personId = $person->id;;
        $this->_em->remove($person);
        $this->_em->flush();

        $this->assertNull($this->_em->find(DDC93Person::CLASSNAME, $personId));
    }

    public function testLoadDql()
    {
        for ($i = 0; $i < 3; $i++) {
            $person = new DDC93Person();
            $person->name = "Donkey Kong$i";
            $person->address = new DDC93Address();
            $person->address->street = "Tree";
            $person->address->zip = "12345";
            $person->address->city = "funkytown";
            $person->address->country = new DDC93Country('United States of America');

            $this->_em->persist($person);
        }

        $this->_em->flush();
        $this->_em->clear();

        $dql = "SELECT p FROM " . __NAMESPACE__ . "\DDC93Person p";
        $persons = $this->_em->createQuery($dql)->getResult();

        $this->assertCount(3, $persons);
        foreach ($persons as $person) {
            $this->assertInstanceOf(DDC93Address::CLASSNAME, $person->address);
            $this->assertEquals('Tree', $person->address->street);
            $this->assertEquals('12345', $person->address->zip);
            $this->assertEquals('funkytown', $person->address->city);
            $this->assertInstanceOf(DDC93Country::CLASSNAME, $person->address->country);
            $this->assertEquals('United States of America', $person->address->country->name);
        }

        $dql = "SELECT p FROM " . __NAMESPACE__ . "\DDC93Person p";
        $persons = $this->_em->createQuery($dql)->getArrayResult();

        foreach ($persons as $person) {
            $this->assertEquals('Tree', $person['address.street']);
            $this->assertEquals('12345', $person['address.zip']);
            $this->assertEquals('funkytown', $person['address.city']);
            $this->assertEquals('United States of America', $person['address.country.name']);
        }
    }

    /**
     * @group dql
     */
    public function testDqlOnEmbeddedObjectsField()
    {
        if ($this->isSecondLevelCacheEnabled) {
            $this->markTestSkipped('SLC does not work with UPDATE/DELETE queries through EM.');
        }

        $person = new DDC93Person('Johannes', new DDC93Address('Moo', '12345', 'Karlsruhe', new DDC93Country('Germany')));
        $this->_em->persist($person);
        $this->_em->flush($person);

        // SELECT
        $selectDql = "SELECT p FROM " . __NAMESPACE__ ."\\DDC93Person p WHERE p.address.city = :city AND p.address.country.name = :country";
        $loadedPerson = $this->_em->createQuery($selectDql)
            ->setParameter('city', 'Karlsruhe')
            ->setParameter('country', 'Germany')
            ->getSingleResult();
        $this->assertEquals($person, $loadedPerson);

        $this->assertNull(
            $this->_em->createQuery($selectDql)
                ->setParameter('city', 'asdf')
                ->setParameter('country', 'Germany')
                ->getOneOrNullResult()
        );

        // UPDATE
        $updateDql = "UPDATE " . __NAMESPACE__ . "\\DDC93Person p SET p.address.street = :street, p.address.country.name = :country WHERE p.address.city = :city";
        $this->_em->createQuery($updateDql)
            ->setParameter('street', 'Boo')
            ->setParameter('country', 'DE')
            ->setParameter('city', 'Karlsruhe')
            ->execute();

        $this->_em->refresh($person);
        $this->assertEquals('Boo', $person->address->street);
        $this->assertEquals('DE', $person->address->country->name);

        // DELETE
        $this->_em->createQuery("DELETE " . __NAMESPACE__ . "\\DDC93Person p WHERE p.address.city = :city AND p.address.country.name = :country")
            ->setParameter('city', 'Karlsruhe')
            ->setParameter('country', 'DE')
            ->execute();

        $this->_em->clear();
        $this->assertNull($this->_em->find(__NAMESPACE__.'\\DDC93Person', $person->id));
    }

    public function testDqlWithNonExistentEmbeddableField()
    {
        $this->setExpectedException('Doctrine\ORM\Query\QueryException', 'no field or association named address.asdfasdf');

        $this->_em->createQuery("SELECT p FROM " . __NAMESPACE__ . "\\DDC93Person p WHERE p.address.asdfasdf IS NULL")
            ->execute();
    }

    public function testEmbeddableWithInheritance()
    {
        $car = new DDC93Car(new DDC93Address('Foo', '12345', 'Asdf'));
        $this->_em->persist($car);
        $this->_em->flush($car);

        $reloadedCar = $this->_em->find(__NAMESPACE__.'\\DDC93Car', $car->id);
        $this->assertEquals($car, $reloadedCar);
    }

    public function testInlineEmbeddableWithPrefix()
    {
        $metadata = $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC3028PersonWithPrefix');

        $this->assertEquals('foobar_id', $metadata->getColumnName('id.id'));
        $this->assertEquals('bloo_foo_id', $metadata->getColumnName('nested.nestedWithPrefix.id'));
        $this->assertEquals('bloo_nestedWithEmptyPrefix_id', $metadata->getColumnName('nested.nestedWithEmptyPrefix.id'));
        $this->assertEquals('bloo_id', $metadata->getColumnName('nested.nestedWithPrefixFalse.id'));
    }

    public function testInlineEmbeddableEmptyPrefix()
    {
        $metadata = $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC3028PersonEmptyPrefix');

        $this->assertEquals('id_id', $metadata->getColumnName('id.id'));
        $this->assertEquals('nested_foo_id', $metadata->getColumnName('nested.nestedWithPrefix.id'));
        $this->assertEquals('nested_nestedWithEmptyPrefix_id', $metadata->getColumnName('nested.nestedWithEmptyPrefix.id'));
        $this->assertEquals('nested_id', $metadata->getColumnName('nested.nestedWithPrefixFalse.id'));
    }

    public function testInlineEmbeddablePrefixFalse()
    {
        $expectedColumnName = 'id';

        $actualColumnName = $this->_em
            ->getClassMetadata(__NAMESPACE__ . '\DDC3028PersonPrefixFalse')
            ->getColumnName('id.id');

        $this->assertEquals($expectedColumnName, $actualColumnName);
    }

    public function testInlineEmbeddableInMappedSuperClass()
    {
        $isFieldMapped = $this->_em
            ->getClassMetadata(__NAMESPACE__ . '\DDC3027Dog')
            ->hasField('address.street');

        $this->assertTrue($isFieldMapped);
    }

    /**
     * @dataProvider getInfiniteEmbeddableNestingData
     */
    public function testThrowsExceptionOnInfiniteEmbeddableNesting($embeddableClassName, $declaredEmbeddableClassName)
    {
        $this->setExpectedException(
            'Doctrine\ORM\Mapping\MappingException',
            sprintf(
                'Infinite nesting detected for embedded property %s::nested. ' .
                'You cannot embed an embeddable from the same type inside an embeddable.',
                __NAMESPACE__ . '\\' . $declaredEmbeddableClassName
            )
        );

        $this->_schemaTool->createSchema(array(
            $this->_em->getClassMetadata(__NAMESPACE__ . '\\' . $embeddableClassName),
        ));
    }

    public function getInfiniteEmbeddableNestingData()
    {
        return array(
            array('DDCInfiniteNestingEmbeddable', 'DDCInfiniteNestingEmbeddable'),
            array('DDCNestingEmbeddable1', 'DDCNestingEmbeddable4'),
        );
    }
}


/**
 * @Entity
 */
class DDC93Person
{
    const CLASSNAME = __CLASS__;

    /** @Id @GeneratedValue @Column(type="integer") */
    public $id;

    /** @Column(type="string") */
    public $name;

    /** @Embedded(class="DDC93Address") */
    public $address;

    /** @Embedded(class = "DDC93Timestamps") */
    public $timestamps;

    public function __construct($name = null, DDC93Address $address = null)
    {
        $this->name = $name;
        $this->address = $address;
        $this->timestamps = new DDC93Timestamps(new \DateTime);
    }
}

/**
 * @Embeddable
 */
class DDC93Timestamps
{
    /** @Column(type = "datetime") */
    public $createdAt;

    public function __construct(\DateTime $createdAt)
    {
        $this->createdAt = $createdAt;
    }
}

/**
 * @Entity
 *
 * @InheritanceType("SINGLE_TABLE")
 * @DiscriminatorColumn(name = "t", type = "string", length = 10)
 * @DiscriminatorMap({
 *     "v" = "Doctrine\Tests\ORM\Functional\DDC93Car",
 * })
 */
abstract class DDC93Vehicle
{
    /** @Id @GeneratedValue(strategy = "AUTO") @Column(type = "integer") */
    public $id;

    /** @Embedded(class = "DDC93Address") */
    public $address;

    public function __construct(DDC93Address $address)
    {
        $this->address = $address;
    }
}

/**
 * @Entity
 */
class DDC93Car extends DDC93Vehicle
{
}

/**
 * @Embeddable
 */
class DDC93Country
{
    const CLASSNAME = __CLASS__;

    /**
     * @Column(type="string", nullable=true)
     */
    public $name;

    public function __construct($name = null)
    {
        $this->name = $name;
    }
}

/**
 * @Embeddable
 */
class DDC93Address
{
    const CLASSNAME = __CLASS__;

    /**
     * @Column(type="string")
     */
    public $street;
    /**
     * @Column(type="string")
     */
    public $zip;
    /**
     * @Column(type="string")
     */
    public $city;
    /** @Embedded(class = "DDC93Country") */
    public $country;

    public function __construct($street = null, $zip = null, $city = null, DDC93Country $country = null)
    {
        $this->street = $street;
        $this->zip = $zip;
        $this->city = $city;
        $this->country = $country;
    }
}

/** @Entity */
class DDC93Customer
{
    /** @Id @GeneratedValue @Column(type="integer") */
    private $id;

    /** @Embedded(class = "DDC93ContactInfo", columnPrefix = "contact_info_") */
    private $contactInfo;
}

/** @Embeddable */
class DDC93ContactInfo
{
    const CLASSNAME = __CLASS__;

    /**
     * @Column(type="string")
     */
    public $email;
    /** @Embedded(class = "DDC93Address") */
    public $address;
}

/**
 * @Entity
 */
class DDC3028PersonWithPrefix
{
    const CLASSNAME = __CLASS__;

    /** @Embedded(class="DDC3028Id", columnPrefix = "foobar_") */
    public $id;

    /** @Embedded(class="DDC3028NestedEmbeddable", columnPrefix = "bloo_") */
    public $nested;

    public function __construct(DDC3028Id $id = null, DDC3028NestedEmbeddable $nested = null)
    {
        $this->id = $id;
        $this->nested = $nested;
    }
}

/**
 * @Entity
 */
class DDC3028PersonEmptyPrefix
{
    const CLASSNAME = __CLASS__;

    /** @Embedded(class="DDC3028Id", columnPrefix = "") */
    public $id;

    /** @Embedded(class="DDC3028NestedEmbeddable", columnPrefix = "") */
    public $nested;

    public function __construct(DDC3028Id $id = null, DDC3028NestedEmbeddable $nested = null)
    {
        $this->id = $id;
        $this->nested = $nested;
    }
}

/**
 * @Entity
 */
class DDC3028PersonPrefixFalse
{
    const CLASSNAME = __CLASS__;

    /** @Embedded(class="DDC3028Id", columnPrefix = false) */
    public $id;

    public function __construct(DDC3028Id $id = null)
    {
        $this->id = $id;
    }
}

/**
 * @Embeddable
 */
class DDC3028Id
{
    const CLASSNAME = __CLASS__;

    /**
     * @Id @Column(type="string")
     */
    public $id;

    public function __construct($id = null)
    {
        $this->id = $id;
    }
}

/**
 * @Embeddable
 */
class DDC3028NestedEmbeddable
{
    const CLASSNAME = __CLASS__;

    /** @Embedded(class="DDC3028Id", columnPrefix = "foo_") */
    public $nestedWithPrefix;

    /** @Embedded(class="DDC3028Id", columnPrefix = "") */
    public $nestedWithEmptyPrefix;

    /** @Embedded(class="DDC3028Id", columnPrefix = false) */
    public $nestedWithPrefixFalse;

    public function __construct(
        DDC3028Id $nestedWithPrefix = null,
        DDC3028Id $nestedWithEmptyPrefix = null,
        DDC3028Id $nestedWithPrefixFalse = null
    ) {
        $this->nestedWithPrefix = $nestedWithPrefix;
        $this->nestedWithEmptyPrefix = $nestedWithEmptyPrefix;
        $this->nestedWithPrefixFalse = $nestedWithPrefixFalse;
    }
}

/**
 * @MappedSuperclass
 */
abstract class DDC3027Animal
{
    /** @Id @GeneratedValue(strategy = "AUTO") @Column(type = "integer") */
    public $id;

    /** @Embedded(class = "DDC93Address") */
    public $address;
}

/**
 * @Entity
 */
class DDC3027Dog extends DDC3027Animal
{
}

/**
 * @Embeddable
 */
class DDCInfiniteNestingEmbeddable
{
    /** @Embedded(class="DDCInfiniteNestingEmbeddable") */
    public $nested;
}

/**
 * @Embeddable
 */
class DDCNestingEmbeddable1
{
    /** @Embedded(class="DDC3028Id") */
    public $id1;

    /** @Embedded(class="DDC3028Id") */
    public $id2;

    /** @Embedded(class="DDCNestingEmbeddable2") */
    public $nested;
}

/**
 * @Embeddable
 */
class DDCNestingEmbeddable2
{
    /** @Embedded(class="DDC3028Id") */
    public $id1;

    /** @Embedded(class="DDC3028Id") */
    public $id2;

    /** @Embedded(class="DDCNestingEmbeddable3") */
    public $nested;
}

/**
 * @Embeddable
 */
class DDCNestingEmbeddable3
{
    /** @Embedded(class="DDC3028Id") */
    public $id1;

    /** @Embedded(class="DDC3028Id") */
    public $id2;

    /** @Embedded(class="DDCNestingEmbeddable4") */
    public $nested;
}

/**
 * @Embeddable
 */
class DDCNestingEmbeddable4
{
    /** @Embedded(class="DDC3028Id") */
    public $id1;

    /** @Embedded(class="DDC3028Id") */
    public $id2;

    /** @Embedded(class="DDCNestingEmbeddable1") */
    public $nested;
}