use warnings;

# Before `make install' is performed this script should be runnable with
# `make test'. After `make install' it should work as `perl Lemonldap-NG-Manager.t'

#########################

# change 'tests => 1' to 'tests => last_test_to_print';

use Test::More;
use Test::Output;
use File::Path;
use JSON;

BEGIN {
    use_ok('Lemonldap::NG::Common::Session');
    use_ok('Lemonldap::NG::Common::CliSessions');
}

#########################

# Insert your test code below, the Test::More module is used here so read
# its man page ( perldoc Test::More ) for help writing this test script.

use File::Temp;
my $dir          = File::Temp::tempdir();
my $sessionsdir  = "$dir/sessions";
my $psessionsdir = "$dir/psessions";
mkdir $sessionsdir;
mkdir $psessionsdir;
my $backupFile = "$dir/backup.json";

my $cli = Lemonldap::NG::Common::CliSessions->new(
    conf => {
        globalStorage        => "Apache::Session::File",
        globalStorageOptions => {
            Directory     => $sessionsdir,
            LockDirectory => $sessionsdir,
        },
        persistentStorage        => "Apache::Session::File",
        persistentStorageOptions => {
            Directory     => $psessionsdir,
            LockDirectory => $psessionsdir,
        },
    }
);

sub backup {
    my $file = shift;
    open F, '>', $file or die $!;
    Lemonldap::NG::Common::CliSessions->new(
        conf => {
            globalStorage        => "Apache::Session::File",
            globalStorageOptions => {
                Directory     => $sessionsdir,
                LockDirectory => $sessionsdir,
            },
            persistentStorage        => "Apache::Session::File",
            persistentStorageOptions => {
                Directory     => $psessionsdir,
                LockDirectory => $psessionsdir,
            },
        },
        stdout => *F,
    )->run( "backup", {} );
    close F;
}

# Provision test sessions
my @sessionsOpts = (
    storageModule        => "Apache::Session::File",
    storageModuleOptions => {
        Directory     => $sessionsdir,
        LockDirectory => $sessionsdir,
    },
    kind  => 'SSO',
    force => 1,
);
my @psessionsOpts = (
    storageModule        => "Apache::Session::File",
    storageModuleOptions => {
        Directory     => $psessionsdir,
        LockDirectory => $psessionsdir,
    },
    kind  => 'Persistent',
    force => 1,
);

sub resetSessions {
    Lemonldap::NG::Common::Session->new( {
            @sessionsOpts,
            id   => "1b3231655cebb7a1f783eddf27d254ca",
            info => {
                "uid" => "rtyler",
            }
        }
    );
    Lemonldap::NG::Common::Session->new( {
            @sessionsOpts,
            id   => "9684dd2a6489bf2be2fbdd799a8028e3",
            info => {
                "uid" => "dwho",
            }
        }
    );
    Lemonldap::NG::Common::Session->new( {
            @sessionsOpts,
            id   => "f90f597566f5cce47d9641377776c0c2",
            info => {
                "uid"      => "dwho",
                "deleteme" => 1,
            }
        }
    );
    Lemonldap::NG::Common::Session->new( {
            @sessionsOpts,
            id   => "1234",
            info => {
                "uid" => "foo",
            }
        }
    );
    Lemonldap::NG::Common::Session->new( {
            @sessionsOpts,
            id   => "1235",
            info => {
                "uid" => "foo",
            }
        }
    );

    Lemonldap::NG::Common::Session->new( {
            @psessionsOpts,
            id    => "5efe8af397fc3577e05b483aca964f1b",
            force => 1,
            info  => {
                "_2fDevices" => to_json( [ {
                            'type'     => 'UBK',
                            'epoch'    => 1588691690,
                            '_yubikey' => 'cccccceijfnf',
                            'name'     => 'Imported automatically'
                        },
                        {
                            'name'  => 'MyU2F',
                            'type'  => 'U2F',
                            'epoch' => 1588691728
                        },
                        {
                            '_secret' => 'mnxkiirpswuojr47kkrty7ax34fy2ix7',
                            'name'    => 'MyTOTP',
                            'type'    => 'TOTP',
                            'epoch'   => 1588691728
                        }
                    ]
                ),
                "_oidcConsents" => to_json( [ {
                            'scope' => 'openid email',
                            'rp'    => 'rp-example',
                            'epoch' => 1589288341
                        },
                        {
                            'scope' => 'openid email',
                            'epoch' => 1589291482,
                            'rp'    => 'rp-example2'
                        }
                    ]
                ),
                "_loginHistory" => {
                    'successLogin' => [ {
                            '_utime' => 1710239445,
                            'ipAddr' => '192.168.1.1',
                        },
                        {
                            '_utime' => 1710153045,
                            'ipAddr' => '10.0.0.5',
                        },
                    ],
                    'failedLogin' => [ {
                            '_utime' => 1710235522,
                            'ipAddr' => '192.168.1.100',
                            'error'  => 5,
                        },
                    ],
                },
                "_session_uid" => "dwho",
            }
        }
    );
    Lemonldap::NG::Common::Session->new( {
            @psessionsOpts,
            id    => "8d3bc3b0e14ea2a155f275aa7c07ebee",
            force => 1,
            info  => {
                "_session_uid" => "rtyler",
                "_2fDevices"   => to_json( [ {
                            'type'     => 'UBK',
                            'epoch'    => 1588691690,
                            '_yubikey' => 'cccccceijfnf',
                            'name'     => 'Imported automatically'
                        },
                        {
                            '_secret' => 'mnxkiirpswuojr47kkrty7ax34fy2ix7',
                            'name'    => 'MyTOTP',
                            'type'    => 'TOTP',
                            'epoch'   => 1588691728
                        }
                    ]
                ),
            }
        }
    );
}

resetSessions;

sub getJson {
    my @args = @_;
    my ($str) = Test::Output::output_from( sub { $cli->run(@args); } );
    return $str =~ /^([\w]+)$/ ? $1 : from_json($str);
}

sub getLines {
    my @args = @_;
    my ($str) = Test::Output::output_from( sub { $cli->run(@args); } );
    return [ split /\n/, $str ];
}

my $res;

# Test get
$res = getJson( "get", {}, "f90f597566f5cce47d9641377776c0c2" );

is( @{$res}, 1, "Found one session" );
is(
    $res->[0]->{_session_id},
    "f90f597566f5cce47d9641377776c0c2",
    "Found correct session ID"
);
is( $res->[0]->{deleteme}, 1, "Found deleteme session key" );

# Change backend
$res = getJson(
    "get",
    { backend => 'persistent' },
    "5efe8af397fc3577e05b483aca964f1b"
);
is( @{$res},                   1,      "Found one session" );
is( $res->[0]->{_session_uid}, 'dwho', "Found correct session" );

# Persistent mode
$res = getJson( "get", { persistent => 1 }, "dwho" );
is( @{$res},                   1,      "Found one session" );
is( $res->[0]->{_session_uid}, 'dwho', "Found correct session" );

# Test output field selection
$res = getJson(
    "get",
    { select => [ "uid", "_session_id" ] },
    "f90f597566f5cce47d9641377776c0c2"
);

is( keys %{ $res->[0] }, 2,      "Only selected fields returned" );
is( $res->[0]->{uid},    "dwho", "Found correct UID" );
is(
    $res->[0]->{_session_id},
    "f90f597566f5cce47d9641377776c0c2",
    "Found correct session ID"
);

# Test search
$res = getJson( "search", {} );
is( @{$res}, 5, "Found 5 sessions" );

$res = getJson( "search", { count => 1 } );
is( $res, 5, "Count gives 5 sessions" );

sub searchTest {

    # Test search with different backend
    $res = getJson( "search", { backend => 'persistent' } );
    is( @{$res}, 2, "Found 2 psessions" );

    $res = getJson( "search", { backend => 'persistent', count => 1 } );
    is( $res, 2, "Count gives 2 psessions" );

    # Persistent mode
    $res = getJson( "search", { persistent => 1 } );
    is( @{$res}, 2, "Found 2 psessions" );

    # Test search with where
    $res = getJson( "search", { where => "uid=dwho" } );
    is( @{$res},                                  2, "Found 2 sessions" );
    is( ( grep { $_->{uid} eq "dwho" } @{$res} ), 2, "Both sessions are dwho" );

    # Test search with where and field selection
    $res = getJson( "search",
        { where => "uid=dwho", select => [ "uid", "_session_id" ] } );
    is( @{$res},             2, "Found 2 sessions" );
    is( keys %{ $res->[0] }, 2, "Only selected fields returned" );

    # Test search with ID output
    $res = getLines( "search", { where => "uid=dwho", idonly => 1 } );
    is( @{$res}, 2, "Got two lines" );
    is(
        ( join ':', sort @{$res} ),
        "9684dd2a6489bf2be2fbdd799a8028e3:f90f597566f5cce47d9641377776c0c2",
        "Correct session IDs"
    );
}
subtest 'Search', \&searchTest;

# Backup sessions
backup($backupFile);

# Delete session
$cli->run( 'delete', {},                  "9684dd2a6489bf2be2fbdd799a8028e3" );
$cli->run( 'delete', { persistent => 1 }, "rtyler" );

$res = getJson( "get", {}, "9684dd2a6489bf2be2fbdd799a8028e3" );
is( @{$res}, 0, "Session was removed" );
$res = getJson(
    "get",
    { backend => 'persistent' },
    "8d3bc3b0e14ea2a155f275aa7c07ebee"
);
is( @{$res}, 0, "Session was removed" );

# We should have 2 foo sessions now
$res = getJson( "search", { where => "uid=foo" } );
is( @{$res}, 2, "Found 2 foo sessions" );

# Test delete by filter, remove two foo sessions
$cli->run( 'delete', { where => "uid=foo" } );

# We should have no foo sessions left
$res = getJson( "search", { where => "uid=foo" } );
is( @{$res}, 0, "Found 0 foo sessions" );

# Set key

$cli->run( "setKey", {}, "f90f597566f5cce47d9641377776c0c2",
    "key1", "value1", "deleteme", "newvalue" );
$res = getJson( "get", {}, "f90f597566f5cce47d9641377776c0c2" );
is( $res->[0]->{key1},     "value1",   "New key was set" );
is( $res->[0]->{deleteme}, "newvalue", "Existing key was changed" );

# Delete key
$cli->run( "delKey", {}, "f90f597566f5cce47d9641377776c0c2",
    "key1", "deleteme", "missing" );
$res = getJson( "get", {}, "f90f597566f5cce47d9641377776c0c2" );
is( $res->[0]->{key1},     undef, "Key was removed" );
is( $res->[0]->{deleteme}, undef, "Key was removed" );

# Show 2FA

$res = getJson( "secondfactors", {}, "get", "dwho" );
is( ( keys %{$res} ), 3, "Found two second factors" );
is( ( grep { $_->{type} eq "UBK" } values %{$res} ),  1, "Found one Yubikey" );
is( ( grep { $_->{type} eq "TOTP" } values %{$res} ), 1, "Found one TOTP" );
is( ( grep { $_->{type} eq "U2F" } values %{$res} ),  1, "Found one U2F" );

# Delete 2FA
$cli->run( "secondfactors", {}, "delete", "dwho",
    "MTU4ODY5MTY5MDo6VUJLOjpJbXBvcnRlZCBhdXRvbWF0aWNhbGx5" );
$res = getJson( "secondfactors", {}, "get", "dwho" );
is( ( keys %{$res} ), 2, "Found two second factors" );
is( ( grep { $_->{type} eq "UBK" } values %{$res} ), 0, "Yubikey was removed" );

# Delete 2FA by type
$cli->run( "secondfactors", {}, "delType", "dwho", "U2F" );
$res = getJson( "secondfactors", {}, "get", "dwho" );
is( ( keys %{$res} ), 1, "Found one second factors" );
is( ( grep { $_->{type} eq "U2F" } values %{$res} ),  0, "U2F was removed" );
is( ( grep { $_->{type} eq "TOTP" } values %{$res} ), 1, "TOTP survived" );

$cli->run( "restore", {}, $backupFile );
subtest 'Search after restore', \&searchTest;

# Delete 2FA by type (with search)
resetSessions;
$cli->run( "secondfactors", { where => "_session_uid=dwho" }, "delType",
    "U2F" );
$res = getJson( "secondfactors", {}, "get", "dwho" );
is( ( keys %{$res} ), 2, "Found one second factors" );
is( ( grep { $_->{type} eq "U2F" } values %{$res} ),  0, "U2F was removed" );
is( ( grep { $_->{type} eq "TOTP" } values %{$res} ), 1, "TOTP survived" );

# Delete 2FA by type (with all)
resetSessions;
$cli->run( "secondfactors", { all => 1 }, "delType", "TOTP" );
$res = getJson( "secondfactors", {}, "get", "dwho" );
is( ( keys %{$res} ), 2, "Found two second factors for dwho" );
is( ( grep { $_->{type} eq "TOTP" } values %{$res} ), 0, "TOTP was removed" );
is( ( grep { $_->{type} eq "UBK" } values %{$res} ),  1, "UBK survived" );
$res = getJson( "secondfactors", {}, "get", "rtyler" );
is( ( keys %{$res} ), 1, "Found one second factors for rtyler" );
is( ( grep { $_->{type} eq "TOTP" } values %{$res} ), 0, "TOTP was removed" );
is( ( grep { $_->{type} eq "UBK" } values %{$res} ),  1, "UBK survived" );

# Show consents
$res = getJson( "consents", {}, "get", "dwho" );
is( ( keys %{$res} ), 2, "Found two consents" );

# Delete consents

$cli->run( "consents", {}, "delete", "dwho", "rp-example" );
$res = getJson( "consents", {}, "get", "dwho" );
is( ( keys %{$res} ),     1,     "Found one consent" );
is( $res->{'rp-example'}, undef, "Consent for test-rp removed" );
ok( $res->{'rp-example2'}, "Consent for test-rp2 still present" );

# History tests
resetSessions;
subtest 'History commands' => sub {

    # Test history lastsuccessdate
    my ( $stdout, $stderr ) = Test::Output::output_from(
        sub { $cli->run( "history", {}, "lastsuccessdate", "dwho" ); } );
    like( $stdout, qr/Mar.*2024/, "lastsuccessdate shows date" );
    is( $stderr, '', "No error on lastsuccessdate" );

    # Test history lastfailuredate
    ( $stdout, $stderr ) = Test::Output::output_from(
        sub { $cli->run( "history", {}, "lastfailuredate", "dwho" ); } );
    like( $stdout, qr/Mar.*2024.*\(5\)/,
        "lastfailuredate shows date with error code" );
    is( $stderr, '', "No error on lastfailuredate" );

    # Test history get (JSON output)
    $res = getJson( "history", {}, "get", "dwho" );
    ok( $res->{successLogin}, "history get has successLogin" );
    ok( $res->{failedLogin},  "history get has failedLogin" );
    is( @{ $res->{successLogin} }, 2, "Found 2 successful logins" );
    is( @{ $res->{failedLogin} },  1, "Found 1 failed login" );

    # Check all fields are present
    ok( $res->{successLogin}[0]{_utime}, "successLogin has _utime" );
    ok( $res->{successLogin}[0]{ipAddr}, "successLogin has ipAddr" );
    ok( $res->{successLogin}[0]{date}, "successLogin has human-readable date" );
    is( $res->{failedLogin}[0]{error}, 5, "failedLogin has error code" );

    # Test error case: user without history
    ( $stdout, $stderr ) = Test::Output::output_from(
        sub { $cli->run( "history", {}, "lastsuccessdate", "rtyler" ); } );
    is( $stdout, '', "No stdout on missing history" );
    like( $stderr, qr/No login history found/, "Error message on stderr" );
};

rmtree $dir;
done_testing();
