<?php
require_once __DIR__ . '/Annotations.case.php';
require_once __DIR__ . '/Annotations.Sample.case.php';

use mindplay\annotations\AnnotationFile;
use mindplay\annotations\AnnotationCache;
use mindplay\annotations\AnnotationManager;
use mindplay\annotations\Annotations;
use mindplay\annotations\Annotation;
use mindplay\annotations\standard\ReturnAnnotation;
use mindplay\test\annotations\Package;
use mindplay\test\lib\xTest;
use mindplay\test\lib\xTestRunner;

if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
    require_once __DIR__ . '/traits/namespaced.php';
    require_once __DIR__ . '/traits/toplevel.php';
}

/**
 * This class implements tests for core annotations
 */
class AnnotationsTest extends xTest
{
    const ANNOTATION_EXCEPTION = 'mindplay\annotations\AnnotationException';

    /**
     * Run this test.
     *
     * @param xTestRunner $testRunner Test runner.
     * @return boolean
     */
    public function run(xTestRunner $testRunner)
    {
        $testRunner->startCoverageCollector(__CLASS__);
        $cachePath = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'runtime';

        Annotations::$config = array(
            'cache' => new AnnotationCache($cachePath),
        );

        if (!is_writable($cachePath)) {
            die('cache path is not writable: ' . $cachePath);
        }

        // manually wipe out the cache:
        $pattern = Annotations::getManager()->cache->getRoot() . DIRECTORY_SEPARATOR . '*.annotations.php';

        foreach (glob($pattern) as $path) {
            unlink($path);
        }

        // disable some annotations not used during testing:
        Annotations::getManager()->registry['var'] = false;
        Annotations::getManager()->registry['undefined'] = 'UndefinedAnnotation';
        $testRunner->stopCoverageCollector();

        return parent::run($testRunner);
    }

    protected function testCanResolveAnnotationNames()
    {
        $manager = new AnnotationManager;
        $manager->namespace = ''; // look for annotations in the global namespace
        $manager->suffix = 'Annotation'; // use a suffix for annotation class-names

        $this->check(
            $manager->resolveName('test') === 'TestAnnotation',
            'should capitalize and suffix annotation names'
        );
        $this->check(
            $manager->resolveName('X\Y\Foo') === 'X\Y\FooAnnotation',
            'should suffix fully qualified annotation names'
        );

        $manager->registry['test'] = 'X\Y\Z\TestAnnotation';
        $this->check(
            $manager->resolveName('test') === 'X\Y\Z\TestAnnotation',
            'should respect registered annotation types'
        );
        $this->check(
            $manager->resolveName('Test') === 'X\Y\Z\TestAnnotation',
            'should ignore case of first letter in annotation names'
        );

        $manager->registry['test'] = false;
        $this->check($manager->resolveName('test') === false, 'should respect disabled annotation types');

        $manager->namespace = 'ABC';
        $this->check($manager->resolveName('hello') === 'ABC\HelloAnnotation', 'should default to standard namespace');
    }

    protected function testCanGetAnnotationFile()
    {
        // This test is for an internal API, so we need to perform some invasive maneuvers:

        $manager = Annotations::getManager();

        $manager_reflection = new ReflectionClass($manager);

        $method = $manager_reflection->getMethod('getAnnotationFile');
        $method->setAccessible(true);

        $class_reflection = new ReflectionClass('mindplay\test\Sample\SampleClass');

        // absolute path to the class-file used for testing
        $file_path = $class_reflection->getFileName();

        if (PHP_VERSION_ID < 80000 && getenv('EMULATE_PHP8_PARSER_TOKENS') === 'true') {
            define('T_NAME_QUALIFIED', 'T_NAME_QUALIFIED');
            define('T_NAME_FULLY_QUALIFIED', 'T_NAME_FULLY_QUALIFIED');
        }

        // Now get the AnnotationFile instance:

        /** @var AnnotationFile $file */
        $file = $method->invoke($manager, $file_path);

        $this->check($file instanceof AnnotationFile, 'should be an instance of AnnotationFile');
        $this->check(count($file->data) > 0, 'should contain Annotation data');
        $this->check($file->path === $file_path, 'should reflect path to class-file');
        $this->check($file->namespace === 'mindplay\test\Sample', 'should reflect namespace');
        $this->check(
            $file->uses === array('Test' => 'Test', 'SampleAlias' => 'mindplay\annotations\Annotation'),
            'should reflect use-clause'
        );
    }

    protected function testCanParseAnnotations()
    {
        $manager = new AnnotationManager;
        Package::register($manager);
        $manager->namespace = ''; // look for annotations in the global namespace
        $manager->suffix = 'Annotation'; // use a suffix for annotation class-names

        $parser = $manager->getParser();

        $source = "
            <?php

            namespace foo\\bar;

            use
                baz\\Hat as Zing,
                baz\\Zap;

            /**
             * @doc 123
             * @note('abc')
             * @required
             * @note('xyz');
             */
            class Sample {
                public function test()
                {
                    \$var = null;

                    \$test = function () use (\$var) {
                        // this inline function is here to assert that the parser
                        // won't pick up the use-clause of an inline function
                    };
                }
            }
        ";

        $code = $parser->parse($source, 'inline-test');
        $test = eval($code);

        $this->check($test['#namespace'] === 'foo\bar', 'file namespace should be parsed and cached');
        $this->check(
            $test['#uses'] === array('Zing' => 'baz\Hat', 'Zap' => 'baz\Zap'),
            'use-clauses should be parsed and cached: ' . var_export($test['#uses'], true)
        );

        $this->check($test['foo\bar\Sample'][0]['#name'] === 'doc', 'first annotation is an @doc annotation');
        $this->check($test['foo\bar\Sample'][0]['#type'] === 'DocAnnotation', 'first annotation is a DocAnnotation');
        $this->check($test['foo\bar\Sample'][0]['value'] === 123, 'first annotation has the value 123');

        $this->check($test['foo\bar\Sample'][1]['#name'] === 'note', 'second annotation is an @note annotation');
        $this->check($test['foo\bar\Sample'][1]['#type'] === 'NoteAnnotation', 'second annotation is a NoteAnnotation');
        $this->check($test['foo\bar\Sample'][1][0] === 'abc', 'value of second annotation is "abc"');

        $this->check(
            $test['foo\bar\Sample'][2]['#type'] === 'mindplay\test\annotations\RequiredAnnotation',
            'third annotation is a RequiredAnnotation'
        );

        $this->check($test['foo\bar\Sample'][3]['#type'] === 'NoteAnnotation', 'last annotation is a NoteAnnotation');
        $this->check($test['foo\bar\Sample'][3][0] === 'xyz', 'value of last annotation is "xyz"');
    }

    protected function testCanGetStaticAnnotationManager()
    {
        if (Annotations::getManager() instanceof AnnotationManager) {
            $this->pass();
        } else {
            $this->fail();
        }
    }

    protected function testCanGetAnnotationUsage()
    {
        $usage = Annotations::getUsage('NoteAnnotation');

        $this->check($usage->class === true);
        $this->check($usage->property === true);
        $this->check($usage->method === true);
        $this->check($usage->inherited === true);
        $this->check($usage->multiple === true);
    }

    protected function testAnnotationWithNonUsageAndUsageAnnotations()
    {
        $this->setExpectedException(
            self::ANNOTATION_EXCEPTION,
            "The class 'UsageAndNonUsageAnnotation' must have exactly one UsageAnnotation (no other Annotations are allowed)"
        );

        Annotations::getUsage('UsageAndNonUsageAnnotation');
    }

    protected function testAnnotationWithSingleNonUsageAnnotation()
    {
        $this->setExpectedException(
            self::ANNOTATION_EXCEPTION,
            "The class 'SingleNonUsageAnnotation' must have exactly one UsageAnnotation (no other Annotations are allowed)"
        );

        Annotations::getUsage('SingleNonUsageAnnotation');
    }

    protected function testUsageAnnotationIsInherited()
    {
        $usage = Annotations::getUsage('InheritUsageAnnotation');
        $this->check($usage->method === true);
    }

    protected function testGetUsageOfUndefinedAnnotationClass()
    {
        $this->setExpectedException(
            self::ANNOTATION_EXCEPTION,
            "Annotation type 'NoSuchAnnotation' does not exist"
        );

        Annotations::getUsage('NoSuchAnnotation');
    }

    protected function testAnnotationWithoutUsageAnnotation()
    {
        $this->setExpectedException(
            self::ANNOTATION_EXCEPTION,
            "The class 'NoUsageAnnotation' must have exactly one UsageAnnotation"
        );

        Annotations::getUsage('NoUsageAnnotation');
    }

    protected function testCanGetClassAnnotations()
    {
        $annotations = Annotations::ofClass(new \ReflectionClass('Test'));
        $this->check(count($annotations) > 0, 'from class reflection');

        $annotations = Annotations::ofClass(new Test());
        $this->check(count($annotations) > 0, 'from class object');

        $annotations = Annotations::ofClass('Test');
        $this->check(count($annotations) > 0, 'from class name');
    }

    protected function testCanGetMethodAnnotations()
    {
        $annotations = Annotations::ofMethod(new \ReflectionClass('Test'), 'run');
        $this->check(count($annotations) > 0, 'from class reflection and method name');

        $annotations = Annotations::ofMethod(new \ReflectionMethod('Test', 'run'));
        $this->check(count($annotations) > 0, 'from method reflection');

        $annotations = Annotations::ofMethod(new Test(), 'run');
        $this->check(count($annotations) > 0, 'from class object and method name');

        $annotations = Annotations::ofMethod('Test', 'run');
        $this->check(count($annotations) > 0, 'from class name and method name');
    }

    protected function testGetAnnotationsFromMethodOfNonExistingClass()
    {
        $this->setExpectedException(
            self::ANNOTATION_EXCEPTION,
            "Unable to read annotations from an undefined class 'NonExistingClass'"
        );
        Annotations::ofMethod('NonExistingClass');
    }

    protected function testGetAnnotationsFromNonExistingMethodOfAClass()
    {
        $this->setExpectedException(
            self::ANNOTATION_EXCEPTION,
            'Unable to read annotations from an undefined method Test::nonExistingMethod()'
        );
        Annotations::ofMethod('Test', 'nonExistingMethod');
    }

    protected function testCanGetPropertyAnnotations()
    {
        $annotations = Annotations::ofProperty(new \ReflectionClass('Test'), 'sample');
        $this->check(count($annotations) > 0, 'from class reflection and property name');

        $annotations = Annotations::ofProperty(new \ReflectionProperty('TestBase', 'sample'));
        $this->check(count($annotations) > 0, 'from property reflection');

        $annotations = Annotations::ofProperty(new Test(), 'sample');
        $this->check(count($annotations) > 0, 'from class object and property name');

        $annotations = Annotations::ofProperty('Test', 'sample');
        $this->check(count($annotations) > 0, 'from class name and property name');
    }

    protected function testGetAnnotationsFromPropertyOfNonExistingClass()
    {
        $this->setExpectedException(
            self::ANNOTATION_EXCEPTION,
            "Unable to read annotations from an undefined class 'NonExistingClass'"
        );
        Annotations::ofProperty('NonExistingClass', 'sample');
    }

    public function testGetAnnotationsFromNonExistingPropertyOfExistingClass()
    {
        $this->setExpectedException(
            self::ANNOTATION_EXCEPTION,
            'Unable to read annotations from an undefined property Test::$nonExisting'
        );
        Annotations::ofProperty('Test', 'nonExisting');
    }

    protected function testCanGetFilteredClassAnnotations()
    {
        $anns = Annotations::ofClass('TestBase', 'NoteAnnotation');

        if (!count($anns)) {
            $this->fail('No annotations found');
            return;
        }

        foreach ($anns as $ann) {
            if (!$ann instanceof NoteAnnotation) {
                $this->fail();
            }
        }

        $this->pass();
    }

    protected function testCanGetFilteredMethodAnnotations()
    {
        $anns = Annotations::ofMethod('TestBase', 'run', 'NoteAnnotation');

        if (!count($anns)) {
            $this->fail('No annotations found');
            return;
        }

        foreach ($anns as $ann) {
            if (!$ann instanceof NoteAnnotation) {
                $this->fail();
            }
        }

        $this->pass();
    }

    protected function testCanGetFilteredPropertyAnnotations()
    {
        $anns = Annotations::ofProperty('Test', 'mixed', 'NoteAnnotation');

        if (!count($anns)) {
            $this->fail('No annotations found');
            return;
        }

        foreach ($anns as $ann) {
            if (!$ann instanceof NoteAnnotation) {
                $this->fail();
            }
        }

        $this->pass();
    }

    protected function testCanGetInheritedClassAnnotations()
    {
        $anns = Annotations::ofClass('Test');

        foreach ($anns as $ann) {
            if ($ann->note == 'Applied to the TestBase class') {
                $this->pass();
                return;
            }
        }

        $this->fail();
    }

    protected function testCanGetInheritedMethodAnnotations()
    {
        $anns = Annotations::ofMethod('Test', 'run');

        foreach ($anns as $ann) {
            if ($ann->note == 'Applied to a hidden TestBase method') {
                $this->pass();
                return;
            }
        }

        $this->fail();
    }

    protected function testCanGetInheritedPropertyAnnotations()
    {
        $anns = Annotations::ofProperty('Test', 'sample');

        foreach ($anns as $ann) {
            if ($ann->note == 'Applied to a TestBase member') {
                $this->pass();
                return;
            }
        }

        $this->fail();
    }

    protected function testDoesNotInheritUninheritableAnnotations()
    {
        $anns = Annotations::ofClass('Test');

        if (count($anns) == 0) {
            $this->fail();
            return;
        }

        foreach ($anns as $ann) {
            if ($ann instanceof UninheritableAnnotation) {
                $this->fail();
                return;
            }
        }

        $this->pass();
    }

    protected function testThrowsExceptionIfSingleAnnotationAppliedTwice()
    {
        $this->setExpectedException(
            self::ANNOTATION_EXCEPTION,
            "Only one annotation of 'SingleAnnotation' type may be applied to the same property"
        );

        Annotations::ofProperty('Test', 'only_one');
    }

    protected function testCanOverrideSingleAnnotation()
    {
        $anns = Annotations::ofProperty('Test', 'override_me');

        if (count($anns) != 1) {
            $this->fail(count($anns) . ' annotations found - expected 1');
            return;
        }

        $ann = reset($anns);

        if ($ann->test != 'This annotation overrides the one in TestBase') {
            $this->fail();
        } else {
            $this->pass();
        }
    }

    protected function testCanHandleEdgeCaseInParser()
    {
        // an edge-case was found in the parser - this test asserts that a php-doc style
        // annotation with no trailing characters after it will be parsed correctly.

        $anns = Annotations::ofClass('TestBase', 'DocAnnotation');

        $this->check(count($anns) == 1, 'one DocAnnotation was expected - found ' . count($anns));
    }

    protected function testCanHandleNamespaces()
    {
        // This test asserts that a namespaced class can be annotated, that annotations can
        // be namespaced, and that asking for annotations of a namespaced annotation-type
        // yields the expected result.

        $anns = Annotations::ofClass('mindplay\test\Sample\SampleClass', 'mindplay\test\Sample\SampleAnnotation');

        $this->check(count($anns) == 1, 'one SampleAnnotation was expected - found ' . count($anns));
    }

    protected function testCanUseAnnotationsInDefaultNamespace()
    {
        $manager = new AnnotationManager();
        $manager->namespace = 'mindplay\test\Sample';
        $manager->cache = false;

        $anns = $manager->getClassAnnotations(
            'mindplay\test\Sample\AnnotationInDefaultNamespace',
            'mindplay\test\Sample\SampleAnnotation'
        );

        $this->check(count($anns) == 1, 'one SampleAnnotation was expected - found ' . count($anns));
    }

    protected function testCanIgnoreAnnotations()
    {
        $manager = new AnnotationManager();
        $manager->namespace = 'mindplay\test\Sample';
        $manager->cache = false;

        $manager->registry['ignored'] = false;

        $anns = $manager->getClassAnnotations('mindplay\test\Sample\IgnoreMe');

        $this->check(count($anns) == 0, 'the @ignored annotation should be ignored');
    }

    protected function testCanUseAnnotationAlias()
    {
        $manager = new AnnotationManager();
        $manager->namespace = 'mindplay\test\Sample';
        $manager->cache = false;

        $manager->registry['aliased'] = 'mindplay\test\Sample\SampleAnnotation';

        /** @var Annotation[] $anns */
        $anns = $manager->getClassAnnotations('mindplay\test\Sample\AliasMe');

        $this->check(count($anns) == 1, 'the @aliased annotation should be aliased');
        $this->check(
            get_class($anns[0]) == 'mindplay\test\Sample\SampleAnnotation',
            'returned @aliased annotation should map to mindplay\test\Sample\SampleAnnotation'
        );
    }

    protected function testCanFindAnnotationsByAlias()
    {
        $ann = Annotations::ofProperty('TestBase', 'sample', '@note');

        $this->check(count($ann) === 1, 'TestBase::$sample has one @note annotation');
    }

    protected function testParseUserDefinedClasses()
    {
        $annotations = Annotations::ofClass('TestClassExtendingUserDefined', '@note');

        $this->check(count($annotations) == 2, 'TestClassExtendingUserDefined has two note annotations.');
    }

    protected function testDoNotParseCoreClasses()
    {
        $annotations = Annotations::ofClass('TestClassExtendingCore', '@note');

        $this->check(count($annotations) == 1, 'TestClassExtendingCore has one note annotations.');
    }

    protected function testDoNotParseExtensionClasses()
    {
        $annotations = Annotations::ofClass('TestClassExtendingExtension', '@note');

        $this->check(count($annotations) == 1, 'TestClassExtendingExtension has one note annotations.');
    }

    protected function testGetAnnotationsFromNonExistingClass()
    {
        $this->setExpectedException(
            self::ANNOTATION_EXCEPTION,
            "Unable to read annotations from an undefined class/trait 'NonExistingClass'"
        );
        Annotations::ofClass('NonExistingClass', '@note');
    }

    protected function testGetAnnotationsFromAnInterface()
    {
        $this->setExpectedException(
            self::ANNOTATION_EXCEPTION,
            "Reading annotations from interface 'TestInterface' is not supported"
        );
        Annotations::ofClass('TestInterface', '@note');
    }

    protected function testGetAnnotationsFromTrait()
    {
        if (version_compare(PHP_VERSION, '5.4.0', '<')) {
            $this->pass();
            return;
        }

        $annotations = Annotations::ofClass('SimpleTrait', '@note');

        $this->check(count($annotations) === 1, 'SimpleTrait has one note annotation.');
    }

    protected function testCanGetMethodAnnotationsIncludedFromTrait()
    {
        if (version_compare(PHP_VERSION, '5.4.0', '<')) {
            $this->pass();
            return;
        }

        $annotations = Annotations::ofMethod('SimpleTraitTester', 'runFromTrait');
        $this->check(count($annotations) > 0, 'for unnamespaced trait');

        $annotations = Annotations::ofMethod('SimpleTraitTester', 'runFromAnotherTrait');
        $this->check(count($annotations) > 0, 'for namespaced trait');
    }

    protected function testHandlesMethodInheritanceWithTraits()
    {
        if (version_compare(PHP_VERSION, '5.4.0', '<')) {
            $this->pass();
            return;
        }

        $annotations = Annotations::ofMethod('InheritanceTraitTester', 'baseTraitAndParent');
        $this->check(count($annotations) === 2, 'baseTraitAndParent inherits parent annotations');
        $this->check($annotations[0]->note === 'inheritance-base-trait-tester', 'parent annotation first');
        $this->check($annotations[1]->note === 'inheritance-base-trait', 'trait annotation second');

        $annotations = Annotations::ofMethod('InheritanceTraitTester', 'traitAndParent');
        $this->check(count($annotations) === 2, 'traitAndParent inherits parent annotations');
        $this->check($annotations[0]->note === 'inheritance-base-trait-tester', 'parent annotation first');
        $this->check($annotations[1]->note === 'inheritance-trait', 'trait annotation second');

        $annotations = Annotations::ofMethod('InheritanceTraitTester', 'traitAndChild');
        $this->check(count($annotations) === 1, 'traitAndChild does not inherit trait');
        $this->check($annotations[0]->note === 'inheritance-trait-tester', 'child annotation first');

        $annotations = Annotations::ofMethod('InheritanceTraitTester', 'traitAndParentAndChild');
        $this->check(count($annotations) === 2, 'traitAndParentAndChild does not inherit trait annotation');
        $this->check($annotations[0]->note === 'inheritance-base-trait-tester', 'parent annotation first');
        $this->check($annotations[1]->note === 'inheritance-trait-tester', 'child annotation second');
    }

    protected function testHandlesMethodAliasingWithTraits()
    {
        if (version_compare(PHP_VERSION, '5.4.0', '<')) {
            $this->pass();
            return;
        }

        $annotations = Annotations::ofMethod('AliasTraitTester', 'baseTraitRun');
        $this->check(count($annotations) === 2, 'baseTraitRun inherits annotation');
        $this->check($annotations[0]->note === 'alias-base-trait-tester', 'inherited annotation goes first');
        $this->check($annotations[1]->note === 'alias-base-trait', 'non-inherited annotation goes second');

        $annotations = Annotations::ofMethod('AliasTraitTester', 'traitRun');
        $this->check(count($annotations) === 2, 'traitRun inherits annotation');
        $this->check($annotations[0]->note === 'alias-base-trait-tester', 'inherited annotation goes first');
        $this->check($annotations[1]->note === 'alias-trait', 'non-inherited annotation goes second');

        $annotations = Annotations::ofMethod('AliasTraitTester', 'run');
        $this->check(count($annotations) === 2, 'run inherits annotation');
        $this->check($annotations[0]->note === 'alias-base-trait-tester', 'inherited annotation goes first');
        $this->check($annotations[1]->note === 'alias-trait-tester', 'non-inherited annotation goes second');
    }

    protected function testHandlesConflictedMethodSelectionWithTraits()
    {
        if (version_compare(PHP_VERSION, '5.4.0', '<')) {
            $this->pass();
            return;
        }

        $annotations = Annotations::ofMethod('InsteadofTraitTester', 'baseTrait');
        $this->check(count($annotations) === 2, 'baseTrait inherits annotation');
        $this->check($annotations[0]->note === 'insteadof-base-trait-tester', 'inherited annotation goes first');
        $this->check($annotations[1]->note === 'insteadof-base-trait-b', 'non-inherited annotation goes second');

        $annotations = Annotations::ofMethod('InsteadofTraitTester', 'trate');
        $this->check(count($annotations) === 2, 'trate inherits annotation');
        $this->check($annotations[0]->note === 'insteadof-base-trait-tester', 'inherited annotation goes first');
        $this->check($annotations[1]->note === 'insteadof-trait-a', 'non-inherited annotation goes second');
    }

    protected function testCanGetPropertyAnnotationsIncludedFromTrait()
    {
        if (version_compare(PHP_VERSION, '5.4.0', '<')) {
            $this->pass();
            return;
        }

        $annotations = Annotations::ofProperty('SimpleTraitTester', 'sampleFromTrait');
        $this->check(count($annotations) > 0, 'for unnamespaced trait');

        $annotations = Annotations::ofProperty('SimpleTraitTester', 'sampleFromAnotherTrait');
        $this->check(count($annotations) > 0, 'for namespaced trait');
    }

    protected function testHandlesPropertyConflictWithTraits()
    {
        if (version_compare(PHP_VERSION, '5.4.0', '<')) {
            $this->pass();
            return;
        }

        set_error_handler(function ($errno, $errstring) { });
        require_once __DIR__ . '/traits/property_conflict.php';
        restore_error_handler();

        $annotations = Annotations::ofProperty('PropertyConflictTraitTester', 'traitAndChild');
        $this->check(count($annotations) === 1, 'traitAndChild does not inherit trait');
        $this->check($annotations[0]->note === 'property-conflict-trait-tester', 'child annotation first');

        $annotations = Annotations::ofProperty('PropertyConflictTraitTester', 'traitAndTraitAndParent');
        $this->check(count($annotations) === 2, 'traitAndTraitAndParent inherits parent annotations');
        $this->check($annotations[0]->note === 'property-conflict-base-trait-tester', 'parent annotation first');
        $this->check($annotations[1]->note === 'property-conflict-trait-two', 'first listed trait annotation second');

        $annotations = Annotations::ofProperty('PropertyConflictTraitTester', 'unannotatedTraitAndAnnotatedTrait');
        $this->check(count($annotations) === 0, 'unannotatedTraitAndAnnotatedTrait has no annotations');

        $annotations = Annotations::ofProperty('PropertyConflictTraitTester', 'traitAndParentAndChild');
        $this->check(count($annotations) === 2, 'traitAndParentAndChild does not inherit trait annotation');
        $this->check($annotations[0]->note === 'property-conflict-base-trait-tester', 'parent annotation first');
        $this->check($annotations[1]->note === 'property-conflict-trait-tester', 'child annotation second');
    }

    protected function testDisallowReadingUndefinedAnnotationProperties()
    {
        $nodeAnnotation = new NoteAnnotation();
        $this->setExpectedException(
            self::ANNOTATION_EXCEPTION,
            'NoteAnnotation::$nonExisting is not a valid property name'
        );
        $result = $nodeAnnotation->nonExisting;
    }

    protected function testDisallowWritingUndefinedAnnotationProperties()
    {
        $nodeAnnotation = new NoteAnnotation();
        $this->setExpectedException(
            self::ANNOTATION_EXCEPTION,
            'NoteAnnotation::$nonExisting is not a valid property name'
        );
        $nodeAnnotation->nonExisting = 'new value';
    }

    protected function testAnnotationCacheGetTimestamp()
    {
        $annotationCache = new AnnotationCache(sys_get_temp_dir());
        $annotationCache->store('sample', '');

        $this->check(
            $annotationCache->getTimestamp('sample') > strtotime('midnight'),
            'Annotation cache last update timestamp is not stale'
        );
    }

    protected function testAnnotationFileTypeResolution()
    {
        $annotationFile = new AnnotationFile('', array(
            '#namespace' => 'LevelA\NS',
            '#uses' => array(
                'LevelBClass' => 'LevelB\Class',
            ),
        ));

        $this->check(
            $annotationFile->resolveType('SubNS1\SubClass') == 'LevelA\NS\SubNS1\SubClass',
            'Class in sub-namespace is resolved correctly'
        );

        $this->check(
            $annotationFile->resolveType('\SubNS1\SubClass') == '\SubNS1\SubClass',
            'Fully qualified class name is not changed during resolution'
        );

        $this->check(
            $annotationFile->resolveType('LevelBClass') == 'LevelB\Class',
            'The "uses ..." clause (exact match) is being used during resolution'
        );

        $this->check(
            $annotationFile->resolveType('SomeClass[]') == 'LevelA\NS\SomeClass[]',
            'The [] at then end of data type are preserved during resolution'
        );

        $this->check(
            $annotationFile->resolveType('integer') == 'integer',
            'Simple data type is kept as-is during resolution'
        );
    }

    protected function testAnnotationManagerWithoutCache()
    {
        $this->setExpectedException(
            self::ANNOTATION_EXCEPTION,
            'AnnotationManager::$cache is not configured'
        );

        $annotationManager = new AnnotationManager();
        $annotationManager->getClassAnnotations('NoteAnnotation');
    }

    protected function testUsingNonExistentAnnotation()
    {
        $this->setExpectedException(
            self::ANNOTATION_EXCEPTION,
            "Annotation type 'WrongInterfaceAnnotation' does not implement the mandatory IAnnotation interface"
        );

        Annotations::ofClass('TestClassWrongInterface');
    }

    protected function testReadingFileAwareAnnotation()
    {
        $annotations = Annotations::ofProperty('TestClassFileAwareAnnotation', 'prop', '@TypeAware');

        $this->check(count($annotations) == 1, 'the @TypeAware annotation was found');
        $this->check(
            $annotations[0]->type == 'mindplay\annotations\IAnnotationParser',
            'data type of type-aware annotation was resolved'
        );
    }

    protected function testAnnotationsConstrainedByCorrectUsageAnnotation()
    {
        $annotations = array(new NoteAnnotation());

        $this->assertApplyConstrains($annotations, 'class');
    }

    protected function testAnnotationsConstrainedByClass()
    {
        $annotations = array(new UselessAnnotation());

        $this->setExpectedException(
            self::ANNOTATION_EXCEPTION,
            "Annotation type 'UselessAnnotation' cannot be applied to a class"
        );

        $this->assertApplyConstrains($annotations, AnnotationManager::MEMBER_CLASS);
    }

    protected function testAnnotationsConstrainedByMethod()
    {
        $annotations = array(new UselessAnnotation());

        $this->setExpectedException(
            self::ANNOTATION_EXCEPTION,
            "Annotation type 'UselessAnnotation' cannot be applied to a method"
        );

        $this->assertApplyConstrains($annotations, AnnotationManager::MEMBER_METHOD);
    }

    protected function testAnnotationsConstrainedByProperty()
    {
        $annotations = array(new UselessAnnotation());

        $this->setExpectedException(
            self::ANNOTATION_EXCEPTION,
            "Annotation type 'UselessAnnotation' cannot be applied to a property"
        );

        $this->assertApplyConstrains($annotations, AnnotationManager::MEMBER_PROPERTY);
    }

    protected function assertApplyConstrains(array &$annotations, $memberType)
    {
        $manager = Annotations::getManager();
        $methodReflection = new ReflectionMethod(get_class($manager), 'applyConstraints');
        $methodReflection->setAccessible(true);
        $methodReflection->invokeArgs($manager, array(&$annotations, $memberType));

        $this->check(count($annotations) > 0);
    }

    public function testStopAnnotationPreventsClassLevelAnnotationInheritance()
    {
        $annotations = Annotations::ofClass('SecondClass', '@note');
        $this->check(count($annotations) === 1, 'class level annotation after own "@stop" not present');
        $this->check($annotations[0]->note === 'class-second', 'non-inherited annotation goes first');

        $annotations = Annotations::ofClass('ThirdClass', '@note');
        $this->check(count($annotations) === 2, 'class level annotation after parent "@stop" not present');
        $this->check($annotations[0]->note === 'class-second', 'inherited annotation goes first');
        $this->check($annotations[1]->note === 'class-third', 'non-inherited annotation goes second');
    }

    public function testStopAnnotationPreventsPropertyLevelAnnotationInheritance()
    {
        $annotations = Annotations::ofProperty('SecondClass', 'prop', '@note');
        $this->check(count($annotations) === 1, 'property level annotation after own "@stop" not present');
        $this->check($annotations[0]->note === 'prop-second', 'non-inherited annotation goes first');

        $annotations = Annotations::ofProperty('ThirdClass', 'prop', '@note');
        $this->check(count($annotations) === 2, 'property level annotation after parent "@stop" not present');
        $this->check($annotations[0]->note === 'prop-second', 'inherited annotation goes first');
        $this->check($annotations[1]->note === 'prop-third', 'non-inherited annotation goes second');
    }

    public function testStopAnnotationPreventsMethodLevelAnnotationInheritance()
    {
        $annotations = Annotations::ofMethod('SecondClass', 'someMethod', '@note');
        $this->check(count($annotations) === 1, 'method level annotation after own "@stop" not present');
        $this->check($annotations[0]->note === 'method-second', 'non-inherited annotation goes first');

        $annotations = Annotations::ofMethod('ThirdClass', 'someMethod', '@note');
        $this->check(count($annotations) === 2, 'method level annotation after parent "@stop" not present');
        $this->check($annotations[0]->note === 'method-second', 'inherited annotation goes first');
        $this->check($annotations[1]->note === 'method-third', 'non-inherited annotation goes second');
    }

    public function testDirectAccessToClassIgnoresStopAnnotation()
    {
        $annotations = Annotations::ofClass('FirstClass', '@note');
        $this->check(count($annotations) === 1);
        $this->check($annotations[0]->note === 'class-first');

        $annotations = Annotations::ofProperty('FirstClass', 'prop', '@note');
        $this->check(count($annotations) === 1);
        $this->check($annotations[0]->note === 'prop-first');

        $annotations = Annotations::ofMethod('FirstClass', 'someMethod', '@note');
        $this->check(count($annotations) === 1);
        $this->check($annotations[0]->note === 'method-first');
    }

    protected function testFilterUnresolvedAnnotationClass()
    {
        $annotations = Annotations::ofClass('TestBase', false);

        $this->check($annotations === array(), 'empty annotation list when filtering failed');
    }

    public function testMalformedParamAnnotationThrowsException()
    {
        $this->setExpectedException(
            self::ANNOTATION_EXCEPTION,
            'ParamAnnotation requires a type property'
        );

        Annotations::ofMethod('BrokenParamAnnotationClass', 'brokenParamAnnotation');
    }

    protected function testOrphanedAnnotationsAreIgnored()
    {
        $manager = new AnnotationManager();
        $manager->namespace = 'mindplay\test\Sample';
        $manager->cache = false;

        /** @var Annotation[] $annotations */
        $annotations = $manager->getMethodAnnotations('mindplay\test\Sample\OrphanedAnnotations', 'someMethod');

        $this->check(count($annotations) == 1, 'the @return annotation was found');
        $this->check(
            $annotations[0] instanceof ReturnAnnotation,
            'the @return annotation has correct type'
        );
    }

}

return new AnnotationsTest;
