### ----------------------------------------------------
### -- UpRooted
### -- Licenses: Artistic-2.0
### -- Authors: Paweł bbkr Pabian
### -- File: t/20-tree.rakutest
### ----------------------------------------------------
### -- Chunk 1 of 2
use lib 'lib';

use Test;

use UpRooted::Schema;
use UpRooted::Table;
use UpRooted::Column;
use UpRooted::Relation;
use UpRooted::Path;
use UpRooted::Tree;

plan 11;

subtest 'UpRooted::Path single node' => {

    plan 2;
    
    my $schema = UpRooted::Schema.new( name => 's1' );
    UpRooted::Table.new( :$schema, name => 't1' );
    
    my $path;
    lives-ok {
        $path = UpRooted::Path.new( root-table => $schema.table( 't1' ), leaf-table => $schema.table( 't1' ) );
    }, 'UpRooted::Path created';
    
    is-deeply $path.relations, [], 'No UpRooted::Relations between root UpRooted::Table that is also leaf UpRooted::Table';
    
};

subtest 'UpRooted::Path without relations' => {

    plan 1;
    
    my $schema = UpRooted::Schema.new( name => 's1' );
    UpRooted::Table.new( :$schema, name => 't1' );
    UpRooted::Table.new( :$schema, name => 't2' );
    
    my $path = UpRooted::Path.new( root-table => $schema.table( 't1' ), leaf-table => $schema.table( 't2' ) );
    
    throws-like {
        $path.relations;
    }, Exception, message => 'No UpRooted::Relations in UpRooted::Path between UpRooted::Table t1 and UpRooted::Table t2.', 'UpRooted::Path must have UpRooted::Relations between UpRooted::Tables.';
    
};

subtest 'UpRooted::Path with inconsistent UpRooted::Relations' => {

    plan 3;
    
    my $schema = UpRooted::Schema.new( name => 's1' );
    UpRooted::Table.new( :$schema, name => 't1' );
    UpRooted::Table.new( :$schema, name => 't2' );
    UpRooted::Table.new( :$schema, name => 't3' );
    UpRooted::Column.new( table => $schema.table( 't1' ), name => 'c1', :!is-nullable );
    UpRooted::Column.new( table => $schema.table( 't2' ), name => 'c1', :!is-nullable );
    UpRooted::Column.new( table => $schema.table( 't2' ), name => 'c2', :!is-nullable );
    UpRooted::Column.new( table => $schema.table( 't3' ), name => 'c2', :!is-nullable );
    UpRooted::Relation.new(
        parent-columns => $schema.table( 't1' ).columns( 'c1' ),
        child-columns => $schema.table( 't2' ).columns( 'c1' ),
        name => 'r1'
    );
    UpRooted::Relation.new(
        parent-columns => $schema.table( 't2' ).columns( 'c2' ),
        child-columns => $schema.table( 't3' ).columns( 'c2' ),
        name => 'r2'
    );
    
    my $path = UpRooted::Path.new( root-table => $schema.table( 't1' ), leaf-table => $schema.table( 't3' ) );
    
    throws-like {
        $path.analyze-relations( $schema.table( 't2' ).child-relation( 'r2' ) );
    }, Exception, message => 'UpRooted::Relations root UpRooted::Table is different than t1.', 'UpRooted::Relations chain must start in root UpRooted::Table';
    
    throws-like {
        $path.analyze-relations( $schema.table( 't1' ).child-relation( 'r1' ), $schema.table( 't1' ).child-relation( 'r1' )  );
    }, Exception, message => 'Parent UpRooted::Relation r1 and child UpRooted::Relation r1 are not referring to the same UpRooted::Table.', 'UpRooted::Relations chain must refer to consistent UpRooted::Tables';
    
    throws-like {
        $path.analyze-relations( $schema.table( 't1' ).child-relation( 'r1' ) );
    }, Exception, message => 'UpRooted::Relations leaf UpRooted::Table is different than t3.', 'UpRooted::Relations chain must end in leaf UpRooted::Table';
    
};

subtest 'UpRooted::Path nullability' => {
    
    plan 3;
    
    my $schema = UpRooted::Schema.new( name => 's1' );
    UpRooted::Table.new( :$schema, name => 't1' );
    UpRooted::Table.new( :$schema, name => 't2' );
    UpRooted::Table.new( :$schema, name => 't3' );
    UpRooted::Column.new( table => $schema.table( 't1' ), name => 'c1', :!is-nullable );
    UpRooted::Column.new( table => $schema.table( 't2' ), name => 'c1', :!is-nullable );
    UpRooted::Column.new( table => $schema.table( 't2' ), name => 'c2', :!is-nullable );
    UpRooted::Column.new( table => $schema.table( 't3' ), name => 'c2', :is-nullable );
    UpRooted::Relation.new(
        parent-columns => $schema.table( 't1' ).columns( 'c1' ),
        child-columns => $schema.table( 't2' ).columns( 'c1' ),
        name => 'r1'
    );
    UpRooted::Relation.new(
        parent-columns => $schema.table( 't2' ).columns( 'c2' ),
        child-columns => $schema.table( 't3' ).columns( 'c2' ),
        name => 'r2'
    );

    my $path = UpRooted::Path.new( root-table => $schema.table( 't1' ), leaf-table => $schema.table( 't1' ) );
    ok !$path.is-nullable, 'UpRooted::Path between root UpRooted::Table that is also leaf UpRooted::Table is never nullable';
    
    $path = UpRooted::Path.new( root-table => $schema.table( 't1' ), leaf-table => $schema.table( 't2' ) );
    $path.analyze-relations( $schema.table( 't1' ).child-relation( 'r1' ) );
    ok !$path.is-nullable, 'UpRooted::Path has no nullable UpRooted::Relations';
    
    $path = UpRooted::Path.new( root-table => $schema.table( 't1' ), leaf-table => $schema.table( 't3' ) );
    $path.analyze-relations( $schema.table( 't1' ).child-relation( 'r1' ), $schema.table( 't2' ).child-relation( 'r2' ),  );
    ok $path.is-nullable, 'UpRooted::Path has nullable UpRooted::Relations';

};

subtest 'UpRooted::Path relations analyze' => {
    
    plan 8;
    
    # there are 3 tables: t1, t2, t3
    # t1->t2, t1->t3, t2->t3 are connected by both nullable and not nullable relations
    # (it makes no sense in real schema but makes compact test one)
    
    my $schema = UpRooted::Schema.new( name => 's1' );
    UpRooted::Table.new( :$schema, name => 't1' );
    UpRooted::Table.new( :$schema, name => 't2' );
    UpRooted::Table.new( :$schema, name => 't3' );
    UpRooted::Column.new( table => $schema.table( 't1' ), name => 'c1', :!is-nullable );
    UpRooted::Column.new( table => $schema.table( 't2' ), name => 'c2', :!is-nullable );
    UpRooted::Column.new( table => $schema.table( 't2' ), name => 'c3', :is-nullable );
    UpRooted::Column.new( table => $schema.table( 't3' ), name => 'c4', :!is-nullable );
    UpRooted::Column.new( table => $schema.table( 't3' ), name => 'c5', :is-nullable );
    my $relation_t1_t2_not_nullable = UpRooted::Relation.new(
        parent-columns => $schema.table( 't1' ).columns( 'c1' ),
        child-columns => $schema.table( 't2' ).columns( 'c2' ),
        name => 'r1'
    );
    my $relation_t1_t2_nullable = UpRooted::Relation.new(
        parent-columns => $schema.table( 't1' ).columns( 'c1' ),
        child-columns => $schema.table( 't2' ).columns( 'c3' ),
        name => 'r2'
    );
    my $relation_t1_t3_not_nullable = UpRooted::Relation.new(
        parent-columns => $schema.table( 't1' ).columns( 'c1' ),
        child-columns => $schema.table( 't3' ).columns( 'c4' ),
        name => 'r3'
    );
    my $relation_t1_t3_nullable = UpRooted::Relation.new(
        parent-columns => $schema.table( 't1' ).columns( 'c1' ),
        child-columns => $schema.table( 't3' ).columns( 'c5' ),
        name => 'r4'
    );
    my $relation_t2_t3_not_nullable = UpRooted::Relation.new(
        parent-columns => $schema.table( 't2' ).columns( 'c2' ),
        child-columns => $schema.table( 't3' ).columns( 'c4' ),
        name => 'r5'
    );
    my $relation_t2_t3_nullable = UpRooted::Relation.new(
        parent-columns => $schema.table( 't2' ).columns( 'c2' ),
        child-columns => $schema.table( 't3' ).columns( 'c5' ),
        name => 'r6'
    );
    
    my $path = UpRooted::Path.new( root-table => $schema.table( 't1' ), leaf-table => $schema.table( 't2' ) );
    $path.analyze-relations( $relation_t1_t2_nullable );
    is-deeply $path.relations, [ $relation_t1_t2_nullable ], 'Any UpRooted::Relation is better than none';
    
    $path = UpRooted::Path.new( root-table => $schema.table( 't1' ), leaf-table => $schema.table( 't3' ) );
    $path.analyze-relations( $relation_t1_t2_nullable, $relation_t2_t3_nullable );
    $path.analyze-relations( $relation_t1_t3_nullable );
    is-deeply $path.relations, [ $relation_t1_t3_nullable ], 'Shorter nullable UpRooted::Relations are better than longer nullable ones';
    
    $path = UpRooted::Path.new( root-table => $schema.table( 't1' ), leaf-table => $schema.table( 't3' ) );
    $path.analyze-relations( $relation_t1_t3_nullable );
    $path.analyze-relations( $relation_t1_t2_nullable, $relation_t2_t3_nullable );
    is-deeply $path.relations, [ $relation_t1_t3_nullable ], 'Longer nullable UpRooted::Relations are not better than shorter nullable ones';

    $path = UpRooted::Path.new( root-table => $schema.table( 't1' ), leaf-table => $schema.table( 't3' ) );
    $path.analyze-relations( $relation_t1_t3_nullable );
    $path.analyze-relations( $relation_t1_t2_not_nullable, $relation_t2_t3_not_nullable );
    is-deeply $path.relations, [ $relation_t1_t2_not_nullable, $relation_t2_t3_not_nullable ], 'Not nullable UpRooted::Relations are always better than nullable ones, regardless of length';

    $path = UpRooted::Path.new( root-table => $schema.table( 't1' ), leaf-table => $schema.table( 't3' ) );
    $path.analyze-relations( $relation_t1_t2_not_nullable, $relation_t2_t3_not_nullable );
    $path.analyze-relations( $relation_t1_t3_nullable );
    is-deeply $path.relations, [ $relation_t1_t2_not_nullable, $relation_t2_t3_not_nullable ], 'Nullable UpRooted::Relations are always worse than not nullable ones, regardless of length';

    $path = UpRooted::Path.new( root-table => $schema.table( 't1' ), leaf-table => $schema.table( 't3' ) );
    $path.analyze-relations( $relation_t1_t2_not_nullable, $relation_t2_t3_not_nullable );
    $path.analyze-relations( $relation_t1_t3_not_nullable );
    is-deeply $path.relations, [ $relation_t1_t3_not_nullable ], 'Shorter not nullable UpRooted::Relations are better than longer not nullable ones';
    
    $path = UpRooted::Path.new( root-table => $schema.table( 't1' ), leaf-table => $schema.table( 't3' ) );
    $path.analyze-relations( $relation_t1_t3_not_nullable );
    $path.analyze-relations( $relation_t1_t2_not_nullable, $relation_t2_t3_not_nullable );
    is-deeply $path.relations, [ $relation_t1_t3_not_nullable ], 'Longer not nullable UpRooted::Relations are not better than shorter not nullable ones';

    $relation_t1_t3_not_nullable.is-blocked = True;
    $path = UpRooted::Path.new( root-table => $schema.table( 't1' ), leaf-table => $schema.table( 't3' ) );
    throws-like {
        $path.analyze-relations( $relation_t1_t3_not_nullable )
    }, Exception, message => 'UpRooted::Relation r3 is blocked.', 'Blocked UpRooted::Relation should not be presented for analysis.';

};

subtest 'UpRooted::Path order' => {
    
    plan 4;
    
    my $schema = UpRooted::Schema.new( name => 's1' );
    UpRooted::Table.new( :$schema, name => 't1' );
    UpRooted::Table.new( :$schema, name => 't2' );
    UpRooted::Table.new( :$schema, name => 't3' );
    UpRooted::Column.new( table => $schema.table( 't1' ), name => 'c1', :!is-nullable );
    UpRooted::Column.new( table => $schema.table( 't2' ), name => 'c2', :is-nullable );
    UpRooted::Column.new( table => $schema.table( 't3' ), name => 'c3', :!is-nullable );
    my $relation_t1_t2 = UpRooted::Relation.new(
        parent-columns => $schema.table( 't1' ).columns( 'c1' ),
        child-columns => $schema.table( 't2' ).columns( 'c2' ),
        name => 'r1'
    );
    my $relation_t1_t3 = UpRooted::Relation.new(
        parent-columns => $schema.table( 't1' ).columns( 'c1' ),
        child-columns => $schema.table( 't3' ).columns( 'c3' ),
        name => 'r2'
    );
    my $relation_t2_t3 = UpRooted::Relation.new(
        parent-columns => $schema.table( 't2' ).columns( 'c2' ),
        child-columns => $schema.table( 't3' ).columns( 'c3' ),
        name => 'r3'
    );
    
    my $path = UpRooted::Path.new( root-table => $schema.table( 't1' ), leaf-table => $schema.table( 't1' ) );
    is $path.order, 0, 'Order is 0 for root UpRooted::Table.';
    
    $path = UpRooted::Path.new( root-table => $schema.table( 't1' ), leaf-table => $schema.table( 't2' ) );
    throws-like {
        $path.order;
    }, Exception, message => 'No UpRooted::Relations in UpRooted::Path between UpRooted::Table t1 and UpRooted::Table t2.', 'Order cannot be determined without UpRooted::Relations between UpRooted::Tables.';
    
    $path = UpRooted::Path.new( root-table => $schema.table( 't1' ), leaf-table => $schema.table( 't3' ) );
    $path.analyze-relations( $relation_t1_t2, $relation_t2_t3 );
    is $path.order, 2, 'Order set to longest UpRooted::Relations';
    
    $path.analyze-relations( $relation_t1_t3 );
    ok $path.order > $path.relations.elems, 'Order never decreases even if shorter UpRooted::Relations are found';

};

subtest 'UpRooted::Path loops' => {
    
    plan 18;
    
    my $schema = UpRooted::Schema.new( name => 's1' );
    UpRooted::Table.new( :$schema, name => 't1' );
    UpRooted::Table.new( :$schema, name => 't2' );
    UpRooted::Column.new( table => $schema.table( 't1' ), name => 'c1', :!is-nullable );
    UpRooted::Column.new( table => $schema.table( 't2' ), name => 'c2', :is-nullable );
    my $relation_t1_t1 = UpRooted::Relation.new(
        parent-columns => $schema.table( 't1' ).columns( 'c1' ),
        child-columns => $schema.table( 't1' ).columns( 'c1' ),
        name => 'r1'
    );
    my $relation_t1_t2 = UpRooted::Relation.new(
        parent-columns => $schema.table( 't1' ).columns( 'c1' ),
        child-columns => $schema.table( 't2' ).columns( 'c2' ),
        name => 'r2'
    );
    my $relation_t2_t2 = UpRooted::Relation.new(
        parent-columns => $schema.table( 't2' ).columns( 'c2' ),
        child-columns => $schema.table( 't2' ).columns( 'c2' ),
        name => 'r3'
    );
    my $relation_t2_t1 = UpRooted::Relation.new(
        parent-columns => $schema.table( 't2' ).columns( 'c2' ),
        child-columns => $schema.table( 't1' ).columns( 'c1' ),
        name => 'r4'
    );
    
    my $path = UpRooted::Path.new( root-table => $schema.table( 't1' ), leaf-table => $schema.table( 't1' ) );
    is $path.is-looped, False, 'Root table alone is never looped.';
    
    $path = UpRooted::Path.new( root-table => $schema.table( 't1' ), leaf-table => $schema.table( 't2' ) );
    throws-like {
        $path.is-looped;
    }, Exception, message => 'No UpRooted::Relations in UpRooted::Path between UpRooted::Table t1 and UpRooted::Table t2.', 'Loop cannot be determined without UpRooted::Relations between UpRooted::Tables.';

    $path = UpRooted::Path.new( root-table => $schema.table( 't1' ), leaf-table => $schema.table( 't2' ) );
    throws-like {
        $path.analyze-relations( $relation_t1_t1, $relation_t1_t1, $relation_t1_t2 );
    }, Exception, message => 'UpRooted::Table t1 seen twice before reaching leaf UpRooted::Table.', 'Loop detected along UpRooted::Relations before reaching leaf UpRooted::Table.';
    
    $path = UpRooted::Path.new( root-table => $schema.table( 't1' ), leaf-table => $schema.table( 't1' ) );
    is $path.analyze-relations( $relation_t1_t1 ), False, 'Root loop circuit breaker triggered';
    is $path.is-looped, True, 'UpRooted::Path marked as looped';
    is-deeply $path.relations, [], 'Looped UpRooted::Path does not establish UpRooted::Relations';
    
    $path = UpRooted::Path.new( root-table => $schema.table( 't1' ), leaf-table => $schema.table( 't2' ) );
    is $path.analyze-relations( $relation_t1_t2, $relation_t2_t2 ), False, 'Short loop circuit breaker triggered';
    is $path.is-looped, True, 'UpRooted::Path marked as looped';
    throws-like {
        $path.relations;
    }, Exception, message => 'No UpRooted::Relations in UpRooted::Path between UpRooted::Table t1 and UpRooted::Table t2.', 'Looped UpRooted::Path does not establish UpRooted::Relations';

    $path = UpRooted::Path.new( root-table => $schema.table( 't1' ), leaf-table => $schema.table( 't1' ) );
    is $path.analyze-relations( $relation_t1_t2, $relation_t2_t1 ), False, 'Long loop circuit breaker triggered';
    is $path.is-looped, True, 'UpRooted::Path marked as looped';
    is-deeply $path.relations, [], 'Looped UpRooted::Path does not establish UpRooted::Relations';
    
    $path = UpRooted::Path.new( root-table => $schema.table( 't1' ), leaf-table => $schema.table( 't2' ) );
    is $path.analyze-relations( $relation_t1_t2 ), True, 'No loop detected';
    is $path.is-looped, False, 'UpRooted::Path is not marked as looped';
    is $path.analyze-relations( $relation_t1_t2, $relation_t2_t2 ), False, 'Loop circuit breaker triggered';
    is $path.is-looped, True, 'UpRooted::Path marked as looped';
    is $path.analyze-relations( $relation_t1_t2 ), True, 'No loop detected';
    is $path.is-looped, True, 'UpRooted::Path is still marked as looped';
    
};

subtest 'UpRooted::Path ambiguity' => {
    
    plan 6;
    
    my $schema = UpRooted::Schema.new( name => 's1' );
    UpRooted::Table.new( :$schema, name => 't1' );
    UpRooted::Table.new( :$schema, name => 't2' );
    UpRooted::Table.new( :$schema, name => 't3' );
    UpRooted::Column.new( table => $schema.table( 't1' ), name => 'c1', :!is-nullable );
    UpRooted::Column.new( table => $schema.table( 't2' ), name => 'c2', :is-nullable );
    UpRooted::Column.new( table => $schema.table( 't2' ), name => 'c3', :is-nullable );
    UpRooted::Column.new( table => $schema.table( 't2' ), name => 'c4', :!is-nullable );
    UpRooted::Column.new( table => $schema.table( 't3' ), name => 'c5', :is-nullable );
    UpRooted::Column.new( table => $schema.table( 't3' ), name => 'c6', :!is-nullable );
    my $relation_t1_t2_1 = UpRooted::Relation.new(
        parent-columns => $schema.table( 't1' ).columns( 'c1' ),
        child-columns => $schema.table( 't2' ).columns( 'c2' ),
        name => 'r1'
    );
    my $relation_t1_t2_2 = UpRooted::Relation.new(
        parent-columns => $schema.table( 't1' ).columns( 'c1' ),
        child-columns => $schema.table( 't2' ).columns( 'c3' ),
        name => 'r2'
    );
    my $relation_t1_t2_3 = UpRooted::Relation.new(
        parent-columns => $schema.table( 't1' ).columns( 'c1' ),
        child-columns => $schema.table( 't2' ).columns( 'c4' ),
        name => 'r3'
    );
    my $relation_t2_t3_1 = UpRooted::Relation.new(
        parent-columns => $schema.table( 't2' ).columns( 'c3' ),
        child-columns => $schema.table( 't3' ).columns( 'c6' ),
        name => 'r4'
    );
    my $relation_t2_t3_2 = UpRooted::Relation.new(
        parent-columns => $schema.table( 't2' ).columns( 'c4' ),
        child-columns => $schema.table( 't3' ).columns( 'c5' ),
        name => 'r5'
    );

    # ambiguity happens because of two nullable UpRooted::Relations
    # connecting the same UpRooted::Table pair
    my $path = UpRooted::Path.new( root-table => $schema.table( 't1' ), leaf-table => $schema.table( 't2' ) );
    
    throws-like {
        $path.is-ambiguous;
    }, Exception, message => 'No UpRooted::Relations in UpRooted::Path between UpRooted::Table t1 and UpRooted::Table t2.', 'Ambiguity cannot be determined without UpRooted::Relations between UpRooted::Tables.';
    
    $path.analyze-relations( $relation_t1_t2_1 );
    is $path.is-ambiguous, False, 'UpRooted::Path is not ambiguous if exactly one nullable UpRooted::Relation is found between UpRooted::Tables';
    
    $path.analyze-relations( $relation_t1_t2_2 );
    is $path.is-ambiguous, True, 'UpRooted::Path is ambiguous because two nullable UpRooted::Relations exists between UpRooted::Tables';
    
    $path.analyze-relations( $relation_t1_t2_3 );
    is $path.is-ambiguous, False, 'UpRooted::Path is no longer ambiguous, not nullable UpRooted::Relation found between UpRooted::Tables';

    # ambiguity happens because there are two nullable UpRooted::Relations
    # connecting different UpRooted::Table pairs
    $path = UpRooted::Path.new( root-table => $schema.table( 't1' ), leaf-table => $schema.table( 't3' ) );

    $path.analyze-relations( $relation_t1_t2_3, $relation_t2_t3_2 ); # not nullable, nullable
    is $path.is-ambiguous, False, 'UpRooted::Path is not ambiguous if leaf UpRooted::Table is reachable through exactly one nullable UpRooted::Relations chain';
    
    $path.analyze-relations( $relation_t1_t2_1, $relation_t2_t3_1 ); # nullable, not nullable
    is $path.is-ambiguous, True, 'UpRooted::Path is ambiguous if leaf UpRooted::Table is reachable through more than one nullable UpRooted::Relations chains';

};

# helper to convert UpRooted::Tree to UpRooted::Table names for easy testing
sub path-to-tables ( $tree ) {
    my @path;
    
    for $tree.paths -> $path {
        push @path, [ $path.root-table.name, |$path.relations.map: *.child-table.name ];
    }
    
    return @path;
}

subtest 'UpRooted::Tree basics' => {
    
    plan 4;
    
    my $schema = UpRooted::Schema.new( name => 's1' );
    UpRooted::Table.new( :$schema, name => 't1' );
    UpRooted::Table.new( :$schema, name => 't2' );
    UpRooted::Table.new( :$schema, name => 't3' );
    UpRooted::Column.new( table => $schema.table( 't1' ), name => 'c1', :!is-nullable );
    UpRooted::Column.new( table => $schema.table( 't2' ), name => 'c2', :!is-nullable );
    UpRooted::Column.new( table => $schema.table( 't3' ), name => 'c3', :!is-nullable );
    UpRooted::Relation.new(
        parent-columns => $schema.table( 't1' ).columns( 'c1' ),
        child-columns => $schema.table( 't2' ).columns( 'c2' ),
        name => 'r1'
    );
    UpRooted::Relation.new(
        parent-columns => $schema.table( 't2' ).columns( 'c2' ),
        child-columns => $schema.table( 't3' ).columns( 'c3' ),
        name => 'r2'
    );
    UpRooted::Relation.new(
        parent-columns => $schema.table( 't1' ).columns( 'c1' ),
        child-columns => $schema.table( 't3' ).columns( 'c3' ),
        name => 'r2'
    );

    my $tree = UpRooted::Tree.new( root-table => $schema.table( 't3' ) );
    is-deeply path-to-tables( $tree ), [ [ 't3' ], ], 'Root table UpRooted::Path';
    
    $tree = UpRooted::Tree.new( root-table => $schema.table( 't2' ) );
    is-deeply path-to-tables( $tree ), [ [ 't2' ], [ 't2', 't3' ] ], 'Linear UpRooted::Paths';
    
    # both t2 and t3 may be reached in the best way from t1,
    # but t2 must be reached first as t3 depends on t2
    $tree = UpRooted::Tree.new( root-table => $schema.table( 't1' ) );
    is-deeply path-to-tables( $tree ), [ [ 't1' ], [ 't1', 't2' ], [ 't1', 't3' ] ], 'Diamond UpRooted::Paths';
    ok [<]( $tree.paths.map: *.order ), 'UpRooted::Paths have correct order';
    
};

subtest 'UpRooted::Tree loops' => {
    
    plan 3;

    my $schema = UpRooted::Schema.new( name => 's1' );
    UpRooted::Table.new( :$schema, name => 't1' );
    UpRooted::Column.new( table => $schema.table( 't1' ), name => 'c1', :!is-nullable );
    UpRooted::Relation.new(
        parent-columns => $schema.table( 't1' ).columns( 'c1' ),
        child-columns => $schema.table( 't1' ).columns( 'c1' ),
        name => 'r1'
    );

    # loop is t1 -> t1
    my $tree = UpRooted::Tree.new( root-table => $schema.table( 't1' ) );
    is-deeply path-to-tables( $tree ), [ [ 't1' ], ], 'Short looped UpRooted::Path';

    $schema = UpRooted::Schema.new( name => 's1' );
    UpRooted::Table.new( :$schema, name => 't1' );
    UpRooted::Table.new( :$schema, name => 't2' );
    UpRooted::Column.new( table => $schema.table( 't1' ), name => 'c1', :!is-nullable );
    UpRooted::Column.new( table => $schema.table( 't2' ), name => 'c2', :!is-nullable );
    UpRooted::Relation.new(
        parent-columns => $schema.table( 't1' ).columns( 'c1' ),
        child-columns => $schema.table( 't2' ).columns( 'c2' ),
        name => 'r1'
    );
    UpRooted::Relation.new(
        parent-columns => $schema.table( 't2' ).columns( 'c2' ),
        child-columns => $schema.table( 't1' ).columns( 'c1' ),
        name => 'r2'
    );

    # loop is t1 -> t2 -> t1
    $tree = UpRooted::Tree.new( root-table => $schema.table( 't1' ) );
    is-deeply path-to-tables( $tree ), [ [ 't1' ], [ 't1', 't2' ] ], 'Long looped UpRooted::Path';

    $schema = UpRooted::Schema.new( name => 's1' );
    UpRooted::Table.new( :$schema, name => 't1' );
    UpRooted::Table.new( :$schema, name => 't2' );
    UpRooted::Table.new( :$schema, name => 't3' );
    UpRooted::Column.new( table => $schema.table( 't1' ), name => 'c1', :!is-nullable );
    UpRooted::Column.new( table => $schema.table( 't2' ), name => 'c2', :!is-nullable );
    UpRooted::Column.new( table => $schema.table( 't3' ), name => 'c3', :!is-nullable );
    UpRooted::Relation.new(
        parent-columns => $schema.table( 't1' ).columns( 'c1' ),
        child-columns => $schema.table( 't2' ).columns( 'c2' ),
        name => 'r1'
    );
    UpRooted::Relation.new(
        parent-columns => $schema.table( 't2' ).columns( 'c2' ),
        child-columns => $schema.table( 't3' ).columns( 'c3' ),
        name => 'r2'
    );
    UpRooted::Relation.new(
        parent-columns => $schema.table( 't2' ).columns( 'c2' ),
        child-columns => $schema.table( 't2' ).columns( 'c2' ),
        name => 'r3'
    );

    # longest UpRooted::Path is t1 -> t2 -> t3, but loop is t2 -> t2
    $tree = UpRooted::Tree.new( root-table => $schema.table( 't1' ) );
    is-deeply path-to-tables( $tree ), [ [ 't1' ], [ 't1', 't2' ], [ 't1', 't2', 't3' ] ], 'Inter looped UpRooted::Path';

};

subtest 'UpRooted::Tree with blocked UpRooted::Relations' => {
    
    plan 2;

    my $schema = UpRooted::Schema.new( name => 's1' );
    UpRooted::Table.new( :$schema, name => 't1' );
    UpRooted::Table.new( :$schema, name => 't2' );
    UpRooted::Column.new( table => $schema.table( 't1' ), name => 'c1', :!is-nullable );
    UpRooted::Column.new( table => $schema.table( 't2' ), name => 'c2', :!is-nullable );
    my $r1 = UpRooted::Relation.new(
        parent-columns => $schema.table( 't1' ).columns( 'c1' ),
        child-columns => $schema.table( 't2' ).columns( 'c2' ),
        name => 'r1',
        :is-blocked
    );
    my $r2 = UpRooted::Relation.new(
        parent-columns => $schema.table( 't1' ).