### ----------------------------------------------------
### -- AttrX::Mooish
### -- Licenses: Artistic-2.0
### -- Authors: Vadim Belman
### -- File: t/090-concurrent.custom.rakutest
### ----------------------------------------------------
use v6;
use Test;
use AttrX::Mooish;

use lib $?FILE.IO.parent(1).add("lib"); use CustomHOW;

# Make sure we have enough threads in pool on low-cores CPUs.
BEGIN PROCESS::<$SCHEDULER> = ThreadPoolScheduler.new(:max_threads(400));

plan 2;

subtest "Concurrent instantiation, single instance per thread" => {
    plan 4;

    my $repeats = 10000;
    my $workers = 20;

    my class Foo {
        has atomicint $.builds = 0;
        has Int $.foo is mooish(:lazy, :clearer, :predicate);

        method build-foo {
            ++⚛$!builds;
            42
        }
    }

    my $cnt-lock = Lock.new;

    my $fetches-total = 0;
    my $inits-total = 0;
    my $nonset-total = 0;
    my $predicate-total = 0;

    sub run-test {
        my $fetch-ok = 0;
        my $inits-ok = 0;
        my $predicate-ok = 0;
        my $nonset-ok = 0;
        for ^$repeats {
            my $inst = Foo.new;
            ++$nonset-ok unless $inst.has-foo;
            ++$fetch-ok if $inst.foo == 42;
            ++$predicate-ok if $inst.has-foo;
            $inst.clear-foo;
            ++$nonset-ok unless $inst.has-foo;
            ++$fetch-ok if $inst.foo == 42;
            $inits-ok += $inst.builds;
        }
        $cnt-lock.protect: {
            $inits-total += $inits-ok;
            $fetches-total += $fetch-ok;
            $predicate-total += $predicate-ok;
            $nonset-total += $nonset-ok;
        }
    }

    my @w-ready;
    my $starter = Promise.new;
    my @p;
    for ^$workers -> $id {
        @w-ready[$id] = Promise.new;
        @p.push: start {
            @w-ready[$id].keep;
            await $starter;
            run-test;
        }
    }

    await @w-ready;
    $starter.keep;
    await @p;

    my $expected = $workers * $repeats;
    is $inits-total, 2 * $expected, "all inits passed";
    is $fetches-total, 2 * $expected, "all fetches passed";
    is $predicate-total, $expected, "all predicates passed";
    is $nonset-total, 2 * $expected, "all non-set passed";
}

subtest "Concurrent lazy builds/clears" => {
    my $thread-count = 100;
    plan 2;
    my class Foo {
        has $.foo is mooish(:lazy, :clearer);
        has atomicint $.builds-ran = 0;

        method build-foo {
            ++⚛$!builds-ran;
        }

        method reset {
            self.clear-foo;
            $!builds-ran ⚛= 0;
        }
    }

    sub access-test($inst, $msg) is test-assertion {
        subtest "concurrent access: " ~ $msg => {
            plan 6;
            my $starter = Promise.new;
            my $intermediate1 = Promise.new;
            my $intermediate2 = Promise.new;

            my @workers;
            my @w-ready;
            my @w-paused1;
            my @w-paused2;

            my @values;
            my @values-cleared;
            my $val-lock = Lock::Async.new;

            for ^$thread-count -> $thread-id {
                @w-ready[$thread-id] = my $ready = Promise.new;
                @w-paused1[$thread-id] = my $paused1 = Promise.new;
                @w-paused2[$thread-id] = my $paused2 = Promise.new;
                @workers.push: start {
                    $ready.keep(True);
                    await $starter;
                    # Initialize concurrent read from the attribute.
                    my $v = $inst.foo;
                    $val-lock.protect: { @values.push: $v };
                    $paused1.keep;
                    await $intermediate1;
                    $inst.clear-foo;
                    # Because clearer and builder are not mutually safe we need another pause here to sync all states.
                    $paused2.keep;
                    await $intermediate2;
                    $v = $inst.foo;
                    $val-lock.protect: { @values-cleared.push: $v };
                }
            }

            # Orchestrate the threads
            await @w-ready;
            $starter.keep;
            await @w-paused1;

            is $inst.builds-ran, 1, "build was ran exactly once per instance";
            is $inst.foo, 1, "initialized with the number of builds invoked";

            $intermediate1.keep;
            await @w-paused2;
            $intermediate2.keep;
            await @workers;

            is $inst.builds-ran, 2, "build was ran for the second time after clearing";
            is $inst.foo, 2, "initialized with the number of builds invoked again";

            # This is to make sure there is nothing wrong about the attribute build logic.
            is-deeply @values.List, 1 xx $thread-count, "all threads got the same value";
            is-deeply @values-cleared.List, 2 xx $thread-count, "all threads got the same value after clearing";
        }
    }

    my $foo = Foo.new;
    access-test($foo, "first round");
    $foo.reset;
    access-test($foo, "second round");

}

done-testing;


