#!/usr/bin/perl

# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Koha is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <https://www.gnu.org/licenses>.

use Modern::Perl;
use utf8;
use Test::NoWarnings;
use Test::More tests => 6;

use C4::Context;
use Koha::AuthUtils;
use Koha::Auth::TwoFactorAuth;
use t::lib::Mocks;
use t::lib::Selenium;
use t::lib::TestBuilder;

my @data_to_cleanup;
my $pref_value = C4::Context->preference('TwoFactorAuthentication');

SKIP: {
    eval { require Selenium::Remote::Driver; };
    skip "Selenium::Remote::Driver is needed for selenium tests.", 2 if $@;

    my $builder = t::lib::TestBuilder->new;

    my $library_name = 'my ❤ library';
    my $library = $builder->build_object( { class => 'Koha::Libraries', value => { branchname => $library_name } } );
    my $patron  = $builder->build_object(
        { class => 'Koha::Patrons', value => { flags => 1, branchcode => $library->branchcode } } );
    $patron->flags(1)->store;    # superlibrarian permission
    my $password = Koha::AuthUtils::generate_password( $patron->category );
    t::lib::Mocks::mock_preference( 'RequireStrongPassword', 0 );
    $patron->set_password( { password => $password } );

    push @data_to_cleanup, $patron, $patron->category, $patron->library;

    my $s      = t::lib::Selenium->new( { login => $patron->userid, password => $password } );
    my $driver = $s->driver;

    subtest 'Setup' => sub {
        plan tests => 13;

        my $mainpage = $s->base_url . q|mainpage.pl|;
        $driver->get($mainpage);
        like( $driver->get_title, qr(Log in to Koha), 'Hitting the main page should redirect to the login form' );

        C4::Context->set_preference( 'TwoFactorAuthentication', 'disabled' );

        fill_login_form($s);
        like(
            $driver->get_title, qr(Koha staff interface),
            'Patron with flags superlibrarian should be able to login'
        );

        $driver->get( $s->base_url . q|members/two_factor_auth.pl| );
        like( $driver->get_title, qr(Error 404), 'Must be redirected to 404 is the pref is off' );

        C4::Context->set_preference( 'TwoFactorAuthentication', 'enabled' );
        $driver->get( $s->base_url . q|members/two_factor_auth.pl| );
        like( $driver->get_title, qr(Two-factor authentication), 'Must be on the page with the pref on' );

        is(
            $driver->find_element(
                '//div[@id="registration-status-disabled"]/div[@class="alert alert-info two-factor-status"]')->get_text,
            'Status: Disabled',
            '2FA is disabled'
        );

        is(
            $driver->find_element(
                '//div[@id="registration-status-enabled"]/div[@class="alert alert-info two-factor-status"]')->get_text,
            '',    # 'Status: Enabled' is not shown
            '2FA is disabled'
        );

        $driver->find_element('//*[@id="enable-2FA"]')->click;
        $s->wait_for_ajax;
        ok( $driver->find_element('//img[@id="qr_code"]'), 'There is a QR code' );
        is( $driver->find_element('//span[@id="issuer"]')->get_text, $library_name );

        $driver->find_element('//*[@id="pin_code"]')->send_keys('wrong_code');
        $driver->find_element('//*[@id="register-2FA"]')->click;
        $s->wait_for_ajax;
        ok( $driver->find_element('//div[@class="alert alert-error"][contains(text(), "Invalid PIN code")]') );
        is( $patron->get_from_storage->secret, undef, 'secret is not set in DB yet' );

        my $secret32 = $driver->find_element('//*[@id="secret32"]')->get_value();
        my $auth     = Koha::Auth::TwoFactorAuth->new( { patron => $patron, secret32 => $secret32 } );
        my $code     = $auth->code();
        $driver->find_element('//*[@id="pin_code"]')->clear;
        $driver->find_element('//*[@id="pin_code"]')->send_keys($code);
        $driver->find_element('//*[@id="register-2FA"]')->click;

        # Wait for the response then go to the page, don't wait for the redirect
        $s->wait_for_ajax;
        $driver->get( $s->base_url . q|members/two_factor_auth.pl| );
        is(
            $driver->find_element(
                '//div[@id="registration-status-disabled"]/div[@class="alert alert-info two-factor-status"]')->get_text,
            '',    # 'Status: Disabled' is not shown
            '2FA is enabled'
        );

        is(
            $driver->find_element(
                '//div[@id="registration-status-enabled"]/div[@class="alert alert-info two-factor-status"]')->get_text,
            'Status: Enabled',
            '2FA is enabled'
        );

        $patron = $patron->get_from_storage;
        is( $patron->decoded_secret, $secret32, 'encrypted secret is set in DB' );

    };

    subtest 'Login' => sub {
        plan tests => 18;

        my $mainpage = $s->base_url . q|mainpage.pl|;

        my $secret32 = $patron->decoded_secret;
        {    # ok first try
            $driver->get( $mainpage . q|?logout.x=1| );
            $driver->get( $s->base_url . q|circ/circulation.pl?borrowernumber=| . $patron->borrowernumber );
            like( $driver->get_title, qr(Log in to Koha), 'Must be on the first auth screen' );
            fill_login_form($s);
            like( $driver->get_title, qr(Two-factor authentication), 'Must be on the second auth screen' );
            is( login_error($s), undef );

            my $auth = Koha::Auth::TwoFactorAuth->new( { patron => $patron, secret32 => $secret32 } );
            my $code = $auth->code();
            $auth->clear;
            $driver->find_element('//form[@id="loginform"]//input[@id="otp_token"]')->send_keys($code);
            $driver->find_element('//input[@type="submit"]')->click;
            like( $driver->get_title, qr(Checking out to ), 'Must be redirected to the original page' );
        }

        {    # second try and logout
            $driver->get( $mainpage . q|?logout.x=1| );
            $driver->get( $s->base_url . q|circ/circulation.pl?borrowernumber=| . $patron->borrowernumber );
            like( $driver->get_title, qr(Log in to Koha), 'Must be on the first auth screen' );
            fill_login_form($s);
            like( $driver->get_title, qr(Two-factor authentication), 'Must be on the second auth screen' );
            is( login_error($s), undef );
            $driver->find_element('//form[@id="loginform"]//input[@id="otp_token"]')->send_keys('wrong_code');
            $driver->find_element('//input[@type="submit"]')->click;
            is( login_error($s), "Invalid two-factor code" );

            $driver->get($mainpage);
            like( $driver->get_title, qr(Two-factor authentication), 'Must still be on the second auth screen' );
            is( login_error($s), undef );
            $driver->find_element('//a[@id="logout"]')->click();
            like( $driver->get_title, qr(Log in to Koha), 'Must be on the first auth screen' );
            is( login_error($s), undef );
        }

        {    # second try and success

            $driver->get( $mainpage . q|?logout.x=1| );
            $driver->get( $s->base_url . q|circ/circulation.pl?borrowernumber=| . $patron->borrowernumber );
            like( $driver->get_title, qr(Log in to Koha), 'Must be on the first auth screen' );
            like( login_error($s), qr(Session timed out) );
            fill_login_form($s);
            like( $driver->get_title, qr(Two-factor authentication), 'Must be on the second auth screen' );
            is( login_error($s), undef );
            $driver->find_element('//form[@id="loginform"]//input[@id="otp_token"]')->send_keys('wrong_code');
            $driver->find_element('//input[@type="submit"]')->click;
            is( login_error($s), "Invalid two-factor code" );

            my $auth = Koha::Auth::TwoFactorAuth->new( { patron => $patron, secret32 => $secret32 } );
            my $code = $auth->code();
            $auth->clear;
            $driver->find_element('//form[@id="loginform"]//input[@id="otp_token"]')->send_keys($code);
            $driver->find_element('//input[@type="submit"]')->click;
            like( $driver->get_title, qr(Checking out to ), 'Must be redirected to the original page' );
        }
    };

    subtest "Send OTP code" => sub {
        plan tests => 3;

        # Make sure the send won't fail because of invalid email addresses
        $patron->library->set(
            {
                branchemail      => 'from@example.org',
                branchreturnpath => undef,
                branchreplyto    => undef,
            }
        )->store;
        $patron->auth_method('two-factor');
        $patron->email(undef);
        $patron->store;

        my $mainpage = $s->base_url . q|mainpage.pl|;
        $driver->get( $mainpage . q|?logout.x=1| );
        like(
            $driver->get_title,
            qr(Log in to Koha),
            'Must be on the first auth screen'
        );
        fill_login_form($s);
        like(
            $driver->get_title,
            qr(Two-factor authentication),
            'Must be on the second auth screen'
        );
        $driver->find_element('//a[@id="send_otp"]')->click;
        $s->wait_for_ajax;
        my $error = $driver->find_element('//div[@id="email_error"]')->get_text;
        like(
            $error,
            qr{Email not sent},
            'Email not sent will display an error'
        );

        # This test will only pass if an SMTP server is defined
        # It cannot be mocked from selenium tests
        #$patron->email('test@example.org');
        #$patron->store;
        #$driver->find_element('//a[@id="send_otp"]')->click;
        #$s->wait_for_ajax;
        #my $message =
        #  $driver->find_element('//div[@id="email_success"]')->get_text;
        #is(
        #    $message,
        #    "The code has been sent by email, please check your inbox.",
        #    'The email must have been sent correctly'
        #);
    };

    subtest "Enforce 2FA setup on first login" => sub {
        plan tests => 7;

        C4::Context->set_preference( 'TwoFactorAuthentication', 'enforced' );

        # Make sure the send won't fail because of invalid email addresses
        $patron->library->set(
            {
                branchemail      => 'from@example.org',
                branchreturnpath => undef,
                branchreplyto    => undef,
            }
        )->store;
        $patron->auth_method('password');
        $patron->email(undef);
        $patron->store;

        my $mainpage = $s->base_url . q|mainpage.pl|;
        $driver->get( $mainpage . q|?logout.x=1| );
        like(
            $driver->get_title,
            qr(Log in to Koha),
            'Must be on the first auth screen'
        );
        fill_login_form($s);
        like(
            $driver->get_title,
            qr(Two-factor authentication setup),
            'Must be on the 2FA auth setup screen'
        );

        $s->wait_for_ajax;    # There is an ajax request to populate the qr_code and the secret
        $driver->set_window_size( 3840, 1080 );
        $s->wait_for_element_visible('//*[@id="registration-form"]');

        isnt( $driver->find_element('//*[@id="qr_code"]')->get_attribute("src"), "" );
        my $secret32 = $driver->find_element('//*[@id="secret32"]')->get_value;

        my $auth     = Koha::Auth::TwoFactorAuth->new( { patron => $patron, secret32 => $secret32 } );
        my $pin_code = $auth->code;

        $driver->find_element('//*[@id="pin_code"]')->send_keys("wrong code");
        $driver->find_element('//*[@id="register-2FA"]')->click;
        $s->wait_for_ajax;
        is(
            $driver->find_element('//*[@id="errors"]')->get_text,
            "Invalid PIN code"
        );

        $driver->find_element('//*[@id="pin_code"]')->clear;
        $driver->find_element('//*[@id="pin_code"]')->send_keys($pin_code);
        $driver->find_element('//*[@id="register-2FA"]')->click;
        is(
            $s->get_next_alert_text,
            "Two-factor authentication correctly configured. You will be redirected to the login screen."
        );
        $driver->accept_alert;

        # FIXME How to test the redirect to the mainpage here

        $patron = $patron->get_from_storage;
        is( $patron->auth_method, 'two-factor', );
        isnt( $patron->secret, undef, );
    };

    subtest "Disable" => sub {
        plan tests => 6;

        my $mainpage = $s->base_url . q|mainpage.pl|;
        $driver->get( $mainpage . q|?logout.x=1| );
        fill_login_form($s);
        my $auth = Koha::Auth::TwoFactorAuth->new( { patron => $patron } );
        my $code = $auth->code();
        $auth->clear;
        $driver->find_element('//form[@id="loginform"]//input[@id="otp_token"]')->send_keys($code);
        $driver->find_element('//input[@type="submit"]')->click;

        $driver->get( $s->base_url . q|members/two_factor_auth.pl| );

        is(
            $driver->find_element(
                '//div[@id="registration-status-disabled"]/div[@class="alert alert-info two-factor-status"]')->get_text,
            '',    # 'Status: Disabled' is not shown
            '2FA is enabled'
        );

        is(
            $driver->find_element(
                '//div[@id="registration-status-enabled"]/div[@class="alert alert-info two-factor-status"]')->get_text,
            'Status: Enabled',
            '2FA is enabled'
        );

        $driver->find_element('//form[@id="two-factor-auth"]//input[@type="submit"]')->click;

        is(
            $driver->find_element(
                '//div[@id="registration-status-disabled"]/div[@class="alert alert-info two-factor-status"]')->get_text,
            'Status: Disabled',
            '2FA is disabled'
        );

        is(
            $driver->find_element(
                '//div[@id="registration-status-enabled"]/div[@class="alert alert-info two-factor-status"]')->get_text,
            '',    # 'Status: Enabled' is not shown
            '2FA is disabled'
        );

        $patron = $patron->get_from_storage;
        is( $patron->secret,        undef,      "Secret has been cleared" );
        is( $patron->auth_method(), 'password', 'auth_method has been reset to "password"' );
    };

    $driver->quit();
}

END {
    $_->delete for @data_to_cleanup;
    C4::Context->set_preference( 'TwoFactorAuthentication', $pref_value );
}

sub login_error {
    my ($s) = @_;
    my $driver = $s->driver;

    $s->remove_error_handler;
    my $login_error = eval {
        my $elt = $driver->find_element('//div[@id="login_error"]');
        return $elt->get_text if $elt && $elt->id;
    };
    $s->add_error_handler;
    return $login_error;
}

# Don't use the usual t::lib::Selenium->auth as we don't want the ->get($mainpage) to test the redirect
sub fill_login_form {
    my ($s) = @_;
    $s->fill_form( { userid => $s->login, password => $s->password } );
    $s->driver->find_element('//input[@id="submit-button"]')->click;
}
