use Test::More tests => 6063;

BEGIN {
    use_ok( 'Set::IntSpan::Fast' );
}

# Basic hash based set for testing

package TestSet;

sub new {
    return bless {}, shift;
}

sub add {
    my $self = shift;
    $self->{$_} = 1 for @_;
}

sub add_range {
    my $self = shift;
    die unless ( @_ % 2 ) == 0;
    while ( my ( $from, $to ) = splice( @_, 0, 2 ) ) {
        die unless $from <= $to;
        $self->add( $from .. $to );
    }
}

sub remove {
    my $self = shift;
    delete $self->{$_} for @_;
}

sub remove_range {
    my $self = shift;
    die unless ( @_ % 2 ) == 0;
    while ( my ( $from, $to ) = splice( @_, 0, 2 ) ) {
        die unless $from <= $to;
        $self->remove( $from .. $to );
    }
}

sub as_array_ref {
    my $self = shift;
    return [ sort { $a <=> $b } map { 1 * $_ } keys %$self ];
}

# Extend Set::IntSpace::Fast

package Set::IntSpan::Fast;

sub as_array_ref {
    my $self = shift;
    return [ $self->as_array() ];
}

sub is_sane {
    my $self = shift;
    my $iter = $self->iterate_runs();
    my $last = undef;
    my $urk  = 0;
    while ( my ( $from, $to ) = $iter->() ) {
        if ( $from > $to ) {
            warn "elements out of order ($from, $to)";
            $urk++;
        }
        if ( defined( $last ) && $from <= $last + 1 ) {
            warn "runs overlap ($last, $from)";
            $urk++;
        }
        $last = $to;
    }
    return $urk;
}

package main;

# Simple cases - two ranges overlapping in various ways
for my $i ( -5 .. 5 ) {
    for my $j ( $i .. 5 ) {
        my @set = ( Set::IntSpan::Fast->new(), TestSet->new() );
        $_->add_range( -2, 2 ) for @set;
        is_deeply(
            $set[0]->as_array_ref(),
            $set[1]->as_array_ref(),
            "add init range"
        );
        $_->add_range( $i, $j ) for @set;
        is_deeply(
            $set[0]->as_array_ref(),
            $set[1]->as_array_ref(),
            "add $i to $j"
        );
        is( $set[0]->is_sane(), 0, "sanity check" );
    }
}

# More complex cases - multiple overlaps
for my $i ( -20 .. 20 ) {
    for my $j ( $i .. 20 ) {
        my @set = ( Set::IntSpan::Fast->new(), TestSet->new() );
        for my $s ( @set ) {
            my $gap = 0;
            my $pos = -18;
            while ( $pos < 18 ) {
                $s->add_range( $pos, $pos + $gap );
                $pos += $gap * 2;
                $gap++;
            }

            # Half the time add an extra element
            if ( $j & 1 ) {
                $s->add_range( $pos, $pos );
            }
        }
        is_deeply(
            $set[0]->as_array_ref(),
            $set[1]->as_array_ref(),
            "add init range"
        );
        $_->add_range( $i, $j ) for @set;
        is_deeply(
            $set[0]->as_array_ref(),
            $set[1]->as_array_ref(),
            "add $i to $j"
        );
        is( $set[0]->is_sane(), 0, "sanity check" );
    }
}

# Simple cases - two ranges overlapping in various ways
for my $i ( -5 .. 5 ) {
    for my $j ( $i .. 5 ) {
        my @set = ( Set::IntSpan::Fast->new(), TestSet->new() );
        $_->add_range( -2, 2 ) for @set;
        is_deeply(
            $set[0]->as_array_ref(),
            $set[1]->as_array_ref(),
            "add init range"
        );
        $_->remove_range( $i, $j ) for @set;
        is_deeply(
            $set[0]->as_array_ref(),
            $set[1]->as_array_ref(),
            "remove $i to $j"
        );
        is( $set[0]->is_sane(), 0, "sanity check" );
    }
}

# More complex cases - multiple overlaps
for my $i ( -20 .. 20 ) {
    for my $j ( $i .. 20 ) {
        my @set = ( Set::IntSpan::Fast->new(), TestSet->new() );
        for my $s ( @set ) {
            my $gap = 0;
            my $pos = -18;
            while ( $pos < 18 ) {
                $s->add_range( $pos, $pos + $gap );
                $pos += $gap * 2;
                $gap++;
            }

            # Half the time add an extra element
            if ( $j & 1 ) {
                $s->add_range( $pos, $pos );
            }
        }
        is_deeply(
            $set[0]->as_array_ref(),
            $set[1]->as_array_ref(),
            "add init range"
        );
        $_->remove_range( $i, $j ) for @set;
        is_deeply(
            $set[0]->as_array_ref(),
            $set[1]->as_array_ref(),
            "remove $i to $j"
        );
        is( $set[0]->is_sane(), 0, "sanity check" );
    }
}

# Some psuedorandom cases
srand( 1 );
for ( 1 .. 500 ) {
    my @set = ( Set::IntSpan::Fast->new(), TestSet->new() );
    my @add = map { ( $_, $_ + int( rand( 10 ) ) ) }
      map { int( rand( 10 ) ) } ( 1 .. int( rand( 17 ) ) );
    my @rem = map { ( $_, $_ + int( rand( 10 ) ) ) }
      map { int( rand( 10 ) ) } ( 1 .. int( rand( 17 ) ) );
    $_->add_range( @add )    for @set;
    $_->remove_range( @rem ) for @set;
    is_deeply(
        $set[0]->as_array_ref(),
        $set[1]->as_array_ref(),
        "random ranges"
    );
}
