use Test::More tests => 12128;

use Set::IntSpan::Fast;
use Set::IntSpan::Fast::PP;

# 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::PP;

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;

for my $class ( qw( Set::IntSpan::Fast Set::IntSpan::Fast::PP ) ) {

  {
    my $set = $class->new;
    isa_ok $set, 'Set::IntSpan::Fast::PP';
    isa_ok $set, $class;
  }

  # Simple cases - two ranges overlapping in various ways
  for my $i ( -5 .. 5 ) {
    for my $j ( $i .. 5 ) {
      my @set = ( $class->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 = ( $class->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 = ( $class->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 = ( $class->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 = ( $class->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"
    );
  }
}
