use strict;
use warnings;
use Test::More 0.98;
use Array::2D;

my $builder = Test::More->builder;
binmode $builder->output,         ":encoding(utf8)";
binmode $builder->failure_output, ":encoding(utf8)";
binmode $builder->todo_output,    ":encoding(utf8)";

use Scalar::Util(qw/blessed reftype/);
use List::Util ('uniq');

sub is_blessed {
    my $obj         = shift;
    my $description = shift;
    if ( defined $description ) {
        $description = "blessed correctly: $description";
    }
    else {
        $description = '... and result is blessed correctly';
    }
    is( blessed($obj), 'Array::2D', $description );
}

sub isnt_blessed {
    my $obj         = shift;
    my $description = shift;
    if ( defined $description ) {
        $description = "not blessed: $description";
    }
    else {
        $description = '... and result is not blessed';
    }
    is( blessed($obj), undef, $description );
}

sub a2dcan {
    my @methods = @_;

    if ( @_ == 1 ) {
        note "Testing $_[0]()";
    }
    else {
        note "Testing methods: @_";
    }

    can_ok( 'Array::2D', @_ );
}

my $has_test_fatal;

sub test_exception (&;@) {
    my $code        = shift;
    my $description = shift;
    my $regex       = shift;

    if ( not defined $has_test_fatal ) {
        if ( eval { require Test::Fatal; 1 } ) {
            $has_test_fatal = 1;
        }
        else {
            $has_test_fatal = 0;
        }
    }

  SKIP: {
        skip( 'Test::Fatal not available', 2 ) unless $has_test_fatal;

        my $exception_obj = &Test::Fatal::exception($code);
        #  bypass prototype
        isnt( $exception_obj, undef, $description );
        like( $exception_obj, $regex, "... and it's the expected exception" );

    }

} ## tidy end: sub test_exception (&;@)

# @all_tests is a list rather than a hash (even though it consists of pairs)
# because I want to test the methods in order

sub plan_and_run_generic_tests {
    my @all_tests  = @{ +shift };
    my $defaults_r = shift;

    my $test_count = generic_test_count( \@all_tests, $defaults_r );

    note "result of generic test count: $test_count";

    plan( tests => $test_count );

    run_generic_tests( \@all_tests, $defaults_r );

    done_testing;

}

sub run_generic_tests {

    my @all_tests  = @{ +shift };
    my $defaults_r = shift;

    while (@all_tests) {
        my $method  = shift @all_tests;
        my $tests_r = shift @all_tests;

        a2dcan($method);

        foreach my $test_r ( @{$tests_r} ) {
            generic_test( $method, $test_r, $defaults_r, );
        }

    }

}

my %proper_blessing = ( object => 'Array::2D', ref => undef );

sub generic_test_count {
    my @all_tests  = @{ +shift };
    my $defaults_r = shift;

    my $test_count = 0;

    while (@all_tests) {
        my $method = shift @all_tests;
        my @tests  = @{ shift @all_tests };
        $test_count += 1;
        # one test per method (a2dcan)

        # two tests (obj and ref) per test in %tests
        # That's why each of the below adds two per test and not just one

        foreach my $test_r (@tests) {

            my %t = _get_test_factors( $method, $test_r, $defaults_r );

            if ( exists $t{exception} ) {
                $test_count += 4;
                next;
            }
            # if there's an exception, add two, and skip the rest because
            # they're not used if there's an exception test.

            # all other counts are just placeholders for now
            for ( $t{test_procedure} ) {
                if ( $_ eq 'results' ) {
                    $test_count += 3 * 2;
                    # result is right,
                    # array hasn't changed,
                    # blessing of array hasn't changed
                }
                elsif ( $_ eq 'altered' ) {
                    $test_count += 2 * 2;
                    # array has changed correctly,
                    # blessing of array hasn't changed
                }
                elsif ( $_ eq 'both' ) {
                    $test_count += 3 * 2;
                    # result is right,
                    # array has changed correctly,
                    # blessing of array hasn't changed
                }
                else {    # ($_ eq 'contextual')
                    $test_count += 5 * 2;
                    # result is right,
                    # array hasn't changed,
                    # blessing of array hasn't changed
                    # in-place result is right,
                    # blessing of array after in-place hasn't changed
                }
            } ## tidy end: for ( $t{test_procedure...})

            if (   exists $test_r->{warning}
                or exists $defaults_r->{$method}{warning} )
            {
                $test_count += 2;
                $test_count += 2 if $t{test_procedure} eq 'contextual';
            }
            if ( $t{check_blessing} and $t{test_procedure} ne 'altered' ) {
                $test_count += 2;
            }

        } ## tidy end: foreach my $test_r (@tests)

    } ## tidy end: while (@all_tests)

    return $test_count;

} ## tidy end: sub generic_test_count

my $has_test_warnings;

sub _run_code_and_warn_maybe (&@) {
    my ( $code, $regex, $description ) = @_;

    if ( not defined $regex ) {
        $code->();
        return;
    }

    if ( not defined $has_test_warnings ) {
        if ( eval { require Test::Warnings; 1 } ) {
            Test::Warnings->import(':no_end_test');
            $has_test_warnings = 1;
        }
        else {
            $has_test_warnings = 0;
        }
    }

    if ($has_test_warnings) {
        my $warning = Test::Warnings::warning( $code );
        like( $warning, $regex, "$description: correct warning" )
          or diag "$description: got unexpected warning(s): ",
          explain($warning);
    }
    else {
        $code->();
      SKIP: {
            skip( "$description: skipped: Test::Warnings not available", 1 );
        }

    }
    return;
} ## tidy end: sub _run_code_and_warn_maybe (&@)

sub generic_test {

    my $method = shift;
    my %t = _get_test_factors( $method, @_ );

    # test => results - test results, ensure array doesn't change.
    # test => altered - test array change, ignore results
    # test => both - test results, also test array change
    # test => contextual -- in non-void context, just like 'results' --
    #    array doesn't change, test results.
    #    in void context, array changes to what results
    #    would have been in non-void context.

    # in all cases, array should stay blessed as before.
    # results should be blessed if $t{check_blessing} is true

    my $description = $t{description};    # easier to interpolate

    my @arguments = _get_arguments( \%t );

    my %to_test = (
        object => Array::2D->clone( $t{test_array} ),
        ref    => Array::2D->clone_unblessed( $t{test_array} )
    );

    my %process = (
        object => sub { $to_test{object}->$method(@arguments) },
        ref    => sub { Array::2D->$method( $to_test{ref}, @arguments ) }
    );

    if ( $t{exception} ) {
        test_exception { $process{object}->() } $t{description}, $t{exception};
        test_exception { $process{ref}->() } $t{description},    $t{exception};
        return;
    }

    foreach my $array_type (qw/object ref/) {
        my $returned;
        _run_code_and_warn_maybe(
            sub {
                $returned
                  = $t{returns_a_list}
                  ? [ $process{$array_type}->() ]
                  : $process{$array_type}->();
            },
            $t{warning},
            $description
        );

        if ( $t{test_procedure} ne 'altered' ) {
            is_deeply( $returned, $t{expected},
                "$method: $description: $array_type: correct result" );

            if ( $t{check_blessing} ) {
                if ($t{check_blessing} eq 'always'
                    or (    $t{check_blessing} eq 'as_orignal'
                        and $array_type eq 'object' )
                  )
                {
                    is_blessed($returned);
                }
                elsif ( $t{check_blessing} eq 'as_original' ) {
                    isnt_blessed($returned);
                }
                else {
                    BAIL_OUT 'Unknown blessing check type: '
                      . $t{check_blessing};
                }
            }

        } ## tidy end: if ( $t{test_procedure...})

        if ( $t{test_procedure} eq 'altered' or $t{test_procedure} eq 'both' ) {
            BAIL_OUT 'Bad "altered" test factor'
              unless reftype( $t{altered} ) eq 'ARRAY';
            is_deeply( $to_test{$array_type}, $t{altered},
                "$method: $description: altered $array_type correctly" );
        }
        else {
            is_deeply( $to_test{$array_type}, $t{test_array},
                "... and it did not alter the $array_type" );
        }

        is( blessed( $to_test{$array_type} ),
            $proper_blessing{$array_type},
            "... and blessing of $array_type did not change"
        );

    } ## tidy end: foreach my $array_type (qw/object ref/)

    if ( $t{test_procedure} eq 'contextual' ) {

        %to_test = (
            object => Array::2D->clone( $t{test_array} ),
            ref    => Array::2D->clone_unblessed( $t{test_array} )
        );

        %process = (
            object => sub { $to_test{object}->$method(@arguments) },
            ref    => sub { Array::2D->$method( $to_test{ref}, @arguments ) }
        );

        foreach my $array_type (qw/object ref/) {
            _run_code_and_warn_maybe { $process{$array_type}->() }
            $t{warning}, $description;
            is_deeply( $to_test{$array_type}, $t{expected},
                "$method in place: $description: $array_type: altered correctly"
            );

            is( blessed( $to_test{$array_type} ),
                $proper_blessing{$array_type},
                "... and blessing of $array_type did not change"
            );
        }

    } ## tidy end: if ( $t{test_procedure...})

    return;

} ## tidy end: sub generic_test

my %is_valid_test_factor = map { $_ => 1 } qw[
  altered  arguments      check_blessing description exception
  expected returns_a_list test_procedure test_array  warning
];

# altered - expected value of new altered array
# arguments - arguments to be passed to method
# check_blessing - check to see if results are blessed
# description - text to be displayed in output
# exception - if present, overrides 'test'.  Method should generate exception.
#    Value is a regex to be tested against that exception to make sure it's
#    the right one.
# expected - expected value of results
# returns_a_list - whether results is a list (as row(), col() ) and needs to
# have an arrayref thrown around it before comparing it.
# test - test procedure, below
# test_array - array values to be tested against. This will be cloned into
#   new object and new reference
# warning - Method should generate warning. Value is a regex to be tested
#   against that warning to make sure it's the right one.
#

my %is_valid_test_procedure
  = map { $_ => 1 } qw/results altered both contextual/;
# test => results - test results, ensure array doesn't change
# test => altered - test array change, ignore results
# test => both - test results, also test array change
# test => contextual -- in non-void context, array doesn't change,
#    test results. in void context, array changes to what results
#       would have been in non-void context.

sub _get_test_factors {

    my $method = shift;
    my %t;
    my $test_r = shift;
    my $defaults_r = shift // {};

    my @keys = uniq sort ( keys %$test_r, keys %{ $defaults_r->{$method} } );

    foreach my $test_factor (@keys) {

        BAIL_OUT("Unknown test factor $test_factor")
          if not $is_valid_test_factor{$test_factor};

        if ( exists $test_r->{$test_factor} ) {
            $t{$test_factor} = $test_r->{$test_factor};
        }
        elsif ( exists $defaults_r->{$method}{$test_factor} ) {
            $t{$test_factor} = $defaults_r->{$method}{$test_factor};
        }
    }

    $t{test_procedure} //= 'results';

    BAIL_OUT 'Unknown test procedure ' . $t{test_procedure}
      unless $is_valid_test_procedure{ $t{test_procedure} };

    return %t;

} ## tidy end: sub _get_test_factors

sub _get_arguments {
    my $t_r = shift;

    my @arguments;

    if ( defined $t_r->{arguments} ) {
        if ( ref $t_r->{arguments} eq 'ARRAY' ) {
            @arguments = @{ $t_r->{arguments} };
        }
        else {
            @arguments = $t_r->{arguments};
        }
    }

    return @arguments;

}

1;
