=pod

Test the Result::Simple module with CHECK_ENABLED is truthy.

=cut

use Test2::V0 qw(subtest is like unlike dies done_testing);

use lib "t/lib";
use TestType qw( Int NonEmptyStr );

BEGIN {
    # Enable type check. The default is true.
    # $ENV{RESULT_SIMPLE_CHECK_ENABLED} = 1;
}

use Result::Simple qw( ok err result_for unsafe_unwrap unsafe_unwrap_err chain pipeline );

subtest 'Test `ok` and `err` functions' => sub {
    subtest '`ok` and `err` functions just return values' => sub {
        my ($data, $err) = ok('foo');
        is $data, 'foo';
        is $err, undef;

        ($data, $err) = err('bar');
        is $data, undef;
        is $err, 'bar';
    };

    subtest '`ok` and `err` must be called in list context' => sub {
        like dies { my $data = ok('foo') }, qr/`ok` must be called in list context/;
        like dies { my $err = err('bar') }, qr/`err` must be called in list context/;
    };

    subtest '`ok` and `err` does not allow multiple arguments' => sub {
        like dies { my ($data, $err) = ok('foo', 'bar') }, qr/`ok` does not allow multiple arguments/;
        like dies { my ($data, $err) = err('bar', 'foo') }, qr/`err` does not allow multiple arguments/;
    };

    subtest '`ok` and `err` does not allow no arguments' => sub {
        like dies { my ($data, $err) = ok() }, qr/`ok` does not allow no arguments/;
        like dies { my ($data, $err) = err() }, qr/`err` does not allow no arguments/;
    };

    subtest '`err` does not allow falsy values' => sub {
        like dies { my ($data, $err) = err(0) }, qr/`err` does not allow a falsy value: 0/;
        like dies { my ($data, $err) = err('0') }, qr/`err` does not allow a falsy value: '0'/;
        like dies { my ($data, $err) = err('') }, qr/`err` does not allow a falsy value: ''/;
    };
};

subtest 'Test `result_for` function' => sub {
    # valid cases
    result_for valid => Int, NonEmptyStr;
    sub valid { ok(42) }

    result_for no_error => Int, undef;
    sub no_error { ok(42) }

    # invalid cases
    result_for invalid_ok_type => Int, NonEmptyStr;
    sub invalid_ok_type { ok('foo') }

    result_for invalid_err_type => Int, NonEmptyStr;
    sub invalid_err_type { err(\1) }

    result_for a_few_result => Int, NonEmptyStr;
    sub a_few_result { 'foo' }

    result_for too_many_result => Int, NonEmptyStr;
    sub too_many_result { (1,2,3) }

    result_for never_return_error => Int, undef;
    sub never_return_error { err('foo') }

    subtest 'When a return value satisfies the Result type (T, E), then return the value' => sub {
        my ($data, $err) = valid();
        is $data, 42;
        is $err, undef;
    };

    subtest 'When a return value satisfies the Result type (T, undef), then return the value' => sub {
        my ($data, $err) = no_error();
        is $data, 42;
        is $err, undef;
    };

    subtest 'When a return value does not satisfy the Result type (T, E), then throw a exception' => sub {
        like dies { my ($data, $err) = invalid_ok_type() },    qr!Invalid success result in `invalid_ok_type`: \['foo',undef\]!;
        like dies { my ($data, $err) = invalid_err_type() },   qr!Invalid failure result in `invalid_err_type`: \[undef,\\1\]!;
        like dies { my ($data, $err) = a_few_result() },       qr!Invalid result tuple \(T, E\) in `a_few_result`. Do you forget to call `ok` or `err` function\? Got: \['foo'\]!;
        like dies { my ($data, $err) = too_many_result() },    qr!Invalid result tuple \(T, E\) in `too_many_result`. Do you forget to call `ok` or `err` function\? Got: \[1,2,3\]!;
        like dies { my ($data, $err) = never_return_error() }, qr!Never return error in `never_return_error`: \[undef,'foo'\]!;
    };

    subtest 'Must handle error' => sub {
        like dies { my $result = valid() }, qr/Must handle error in `valid`/;
    };

    subtest '(T, E) requires `check` method' => sub {
        sub invalid_type_T { ok(42) };
        like dies { result_for invalid_type_T => 'Hello', NonEmptyStr }, qr!result_for T requires `check` method!;

        sub invalid_type_E { err(42) };
        like dies { result_for invalid_type_E => Int, 'World' }, qr!result_for E requires `check` method!;
    };

    subtest 'E should not allow falsy values' => sub {
        sub should_not_allow_falsy { err(0) };
        like dies { result_for should_not_allow_falsy => Int, Int }, qr/result_for E should not allow falsy values: \[0,'0'\]/;
    };

    subtest 'Test the details of `retsult_for` function' => sub {
        subtest 'Useful stacktrace' => sub {

            result_for test_stacktrace => Int, NonEmptyStr;
            sub test_stacktrace { Carp::confess('hello') }

            eval { my ($data, $err) = test_stacktrace() };

            my $file = __FILE__;
            like $@, qr!hello at $file line!;
            like $@, qr/main::test_stacktrace\(\) called at $file line /, 'stacktrace includes function name';
            unlike $@, qr/Result::Simple::/, 'stacktrace does not include Result::Simple by Scope::Upper';
        };

        subtest 'Same subname and prototype as original' => sub {

            result_for same => Int, NonEmptyStr;
            sub same (;$) { ok(42) }

            my $code = \&same;

            require Sub::Util;
            my $name = Sub::Util::subname($code);
            is $name, 'main::same';

            my $proto = Sub::Util::prototype($code);
            is $proto, ';$';
        };

        subtest 'When function is not found, then throw a exception' => sub {
            like dies { result_for xxx => Int, NonEmptyStr } => qr/result_for: function `xxx` not found/;
        };
    };
};

subtest 'Test `unsafe_unwrap` function' => sub {
    subtest 'When ok() is called, then return the value' => sub {
        my $got = unsafe_unwrap(ok(42));
        is $got, 42;
    };

    subtest 'When err() is called, then throw a exception' => sub {
        like dies { my ($data, $err) = unsafe_unwrap(err('foo')) }, qr/Error called in `unsafe_unwrap`/;
    };
};

subtest 'Test `unsafe_unwrap_err` function' => sub {
    subtest 'When ok() is called, then throw a exception' => sub {
        like dies { my ($data, $err) = unsafe_unwrap_err(ok(42)) }, qr/No error called in `unsafe_unwrap_err`/;
    };
    subtest 'When err() is called, then return the value' => sub {
        my $got = unsafe_unwrap_err(err('foo'));
        is $got, 'foo';
    };
};

subtest 'Test `chain` function' => sub {
    sub chain_test {
        my $v = shift;
        return err('No more') if $v == 1;
        return ok($v / 2);
    }

    my ($v1, $e1) = chain(chain_test => ok(8));
    is $v1, 4;
    is $e1, undef;

    my ($v2, $e2) = chain(chain_test => ok(1));
    is $v2, undef;
    is $e2, 'No more';

    my ($v3, $e3) = chain(chain_test => err('foo'));
    is $v3, undef;
    is $e3, 'foo';

    like dies { my $v = chain(chain_test => 1, 2) }, qr/`chain` must be called in list context/;
    like dies { my ($v, $e) = chain(chain_test => 1) }, qr/`chain` arguments must be func and result/;
    like dies { my ($v, $e) = chain(unknown => 1, 2) }, qr/Function `unknown` not found in main/;
};

subtest 'Test `pipeline` function' => sub {
    sub pipeline_test {
        my $v = shift;
        return err('No more') if $v == 1;
        return ok($v / 2);
    }

    my $code = pipeline qw( pipeline_test pipeline_test );
    my ($v1, $e1) = $code->(ok(8));
    is $v1, 2;
    is $e1, undef;

    my ($v2, $e2) = $code->(ok(2));
    is $v2, undef;
    is $e2, 'No more';

    my ($v3, $e3) = $code->(ok(1));
    is $v3, undef;
    is $e3, 'No more';

    like dies { my $v = $code->(1, 2) }, qr/pipelined function must be called in list context/;
    like dies { my ($v, $e) = $code->(1) }, qr/pipelined function arguments must be result/;
    like dies { my $c = pipeline qw( unknown ) }, qr/Function `unknown` not found in main/;
};

done_testing;
