#!/usr/local/cpanel/3rdparty/bin/perl -w

# cpanel - t/medium/Cpanel-Koality-Project.t       Copyright 2023 cPanel, L.L.C.
#                                                           All rights reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited

package t::medium::Cpanel::Koality::Project;

use cPstrict;
use Test::Cpanel::Policy;

use Test2::V0;
use Test2::Plugin::NoWarnings;
use Test2::Tools::Exception;
use Data::Dumper;

use Test::MockModule          ();
use Test::MockObject::Extends ();
use Test::MockFile            ();
use File::Temp                ();
use Cpanel::JSON              ();

use Cpanel::Koality::Project ();

use base qw(Test::Class);

my $mock_stage_file = Test::MockFile->file('/var/cpanel/use_koality_stage');
my $mock_errorlog   = Test::MockFile->file( '/usr/local/cpanel/logs/error_log', {} );

my $mock_privs = Test::MockModule->new('Cpanel::AccessIds::ReducedPrivileges');
$mock_privs->redefine( 'new' => sub { return 1 } );

my $mock_validate = Test::MockModule->new('Cpanel::Koality::Validate');
$mock_validate->redefine( 'valid_api_object' => sub { return 1 } );

my $mock_username = Test::MockModule->new('Cpanel::Validate::Username');
$mock_username->redefine( 'user_exists_or_die' => sub { return 1 } );

my $homedir      = File::Temp->newdir();
my $mock_pwcache = Test::MockModule->new('Cpanel::PwCache');
$mock_pwcache->redefine( 'gethomedir' => sub { return $homedir } );

my $mock_fileopts = Test::MockModule->new('Cpanel::Koality::User');
$mock_fileopts->redefine( '_ensure_conf_dir' => sub ($self) { return mkdir( $homedir . '/.koality' ) } );

my $mock_perms = Test::MockModule->new('Cpanel::Transaction::File::Base');
$mock_perms->redefine( '_set_permissions' => sub { return 1 } );

my $locale_mock = Test::MockModule->new('Cpanel::Locale::Utils::User');
$locale_mock->redefine( 'get_user_locale' => sub { return 'en' } );

my $mock_companyid = Test::MockModule->new('Cpanel::License::CompanyID');
$mock_companyid->redefine( 'get_company_id' => sub { return '42' } );

my $mock_serverid = Test::MockModule->new('Cpanel::Plugins::ServerId');
$mock_serverid->redefine( 'get_server_id' => sub { return '101010' } );

my $expected_api_response = {};

__PACKAGE__->new()->runtests() if !caller;

sub test_constructor : Tests(5) {

    my $tests = {
        url            => '!notvalid@url',
        has_alerting   => 1,
        system_type_id => 'A',
        system_size_id => 'B',
        owner          => 'C',
    };

    foreach my $test ( sort keys $tests->%* ) {
        ok( dies { Cpanel::Koality::Project->new( $test => $tests->{$test} ) }, "new: Dies with invalid $test" );
    }

    return;
}

sub test_get_system_types : Tests(5) {

    local $Cpanel::user = 'root';
    my $api = _setup_api_mock();

    $expected_api_response = {
        data => {
            provider_id       => '1234',
            main_system_types => [
                {
                    name => 'Website',
                    id   => '1',
                },
                {
                    name => 'Blog',
                    id   => '2',
                },
            ],
        }
    };

    my $mock_locale = Test::MockModule->new('Cpanel::Locale::Utils::User');
    $mock_locale->redefine( 'get_user_locale' => sub { return 'en' } );

    my $proj = Cpanel::Koality::Project->new( api => $api, user => Cpanel::Koality::User->new( api => $api ) );

    my $types = $proj->get_system_types();

    is( $types->{provider_id}, $expected_api_response->{data}{provider_id}, "get_system_types: Got the expected provider_id" );

    foreach my $system ( $expected_api_response->{data}{main_system_types}->@* ) {
        is( $types->{types}{ $system->{name} }, $system->{id}, "get_system_type: Got the correct id for $system->{name}." );
    }

    ok( dies { $proj->get_system_id_for_type('doesNotExist') }, "get_system_type_by_id: Dies if the type doesn't exist." );

    my $type_id = $proj->get_system_id_for_type('Website');

    is( $type_id, '1', "get_system_type_by_id: Got the expected type id." );

    return;
}

sub test_get_all_scores : Tests(2) {

    local $Cpanel::user = 'root';
    my $api = _setup_api_mock();

    my $mock_proj = Test::MockModule->new('Cpanel::Koality::Project');
    $mock_proj->redefine( get_all_projects => sub { return ( { systems => [ { id => 7240 } ] } ) } );

    $expected_api_response = Cpanel::JSON::Load(
        q|{"data":{"system":{"name":"cPanel","id":7240},"scores":{"basic-content":{"sub_scores":[{"identifier":"Score:Content:Deadlink","title":"Dead Links","message":"We have found few or no dead links on the monitored project pages.","weighted_score":20,"score":100,"status":"good","attributes":{"limit":0,"tool":"DeadLink","components_failed":0,"scoring":"good","score":100,"components_processed":1},"incidents":[],"description":"Dead links, i.e. link to pages that no longer exist, lead to a negative user experience.","weight":20},{"incidents":[],"description":"Faulty elements, such as images that have been included but not found, can severely impact the shopping experience and reduce confidence in the project.","weight":20,"score":100,"attributes":{"components_processed":1,"score":100,"scoring":"good","components_failed":0,"limit":0},"status":"good","title":"Broken resources","weighted_score":20,"message":"We have not found any faulty images, scripts or CSS files in the deposited store project.","identifier":"Score:Content:Brokenresources"}],"master":{"score":100,"attributes":{"score":100,"limit":0},"status":"good","description":"In order to successfully compete with your own project, it should radiate competence, professionalism and trustworthiness already on the first visit. Technical errors such as dead links or faulty resources are out of place.","incidents":[],"identifier":"Score:Basic:Content","title":"Content","message":"From a technical point of view, the project is on a good content level. It was analyzed for dead links and faulty resources."}},"basic-tech":{"master":{"identifier":"Score:Basic:Tech","message":"We could not find any or only very few technical errors in the project. There is no need for action here.","title":"Technology","status":"good","attributes":{"limit":0,"score":100},"score":100,"description":"Modern online projects are becoming increasingly complex in their programming. Often small technical errors can already have a big effect. This score evaluates these errors.","incidents":[]},"sub_scores":[{"score":100,"attributes":{"limit":0,"tool":"JsErrorScanner","components_failed":0,"scoring":"good","score":100,"components_processed":1},"status":"good","incidents":[],"description":"Internal JavaScript errors are errors caused by the system. Such errors can, for example, prevent a customer from completing a purchase or surfing your site. Some of these errors can also be harmless but should be fixed in any case.","weight":20,"identifier":"Score:Tech:Javascriptinternal","title":"Internal JavaScript errors","weighted_score":20,"message":"We have found no or hardly any internal JavaScript errors on the project pages."},{"attributes":{"limit":0,"components_failed":0,"tool":"JsErrorScanner","scoring":"good","score":100,"components_processed":1},"status":"good","score":100,"incidents":[],"description":"External JavaScript errors are generated by third-party developers. This can be, for example, embedded advertisements or even a counting pixel. Often errors indicate a wrong integration. Correcting such errors can be difficult because the source code of these tools is not customizable.","weight":20,"identifier":"Score:Tech:Javascriptexternal","message":"No or hardly any external JavaScript errors were found. External integrations seem to work continuously.","weighted_score":20,"title":"External JavaScript errors"}]},"basic-seo":{"sub_scores":[{"status":"bad","attributes":{"_scoring":"bad","score":-1,"limit":100},"score":-1,"incidents":[],"description":"The Lighthouse tool provided by Google calculates the search engine friendliness of a website based on various criteria.","weight":20,"identifier":"Score:Seo:Lighthouse","weighted_score":0,"message":"This value is currently not calculated.","title":"Google SEO rating"},{"identifier":"Score:Seo:Sitemap","title":"Accuracy of the sitemap","weighted_score":0,"message":"This value is currently not calculated.","score":-1,"attributes":{"components_failed":0,"limit":0,"components_processed":0,"tool":"Sitemap","scoring":"bad","score":-1,"not_calculated_reason":"All components had errors."},"status":"bad","description":"Search engines use XML sitemaps to quickly search entire online projects in a structured manner. In order to always find the current data in Google in time, the sitemaps must be technically correct.","incidents":[],"weight":20},{"identifier":"Score:Seo:Mobilefriendly","title":"Mobile Friendliness","message":"This value is currently not calculated.","weighted_score":0,"score":-1,"attributes":{"limit":0,"score":-1},"status":"good","incidents":[],"description":"More and more search queries are made via mobile devices. For this reason it is important to be listed here by Google and Co. This score is based on Google's own tools.","weight":20}],"master":{"identifier":"Score:Basic:Seo","message":"This value is currently not calculated.","title":"Search engine optimization","status":"bad","attributes":{"limit":0,"score":-1},"score":-1,"incidents":[],"description":"Very often potential customers reach online stores via search engines like Google. In order to be listed well and thus be found, there are a few things to consider. The SEO evaluation summarizes these characteristics and uses Google's own tools."}},"basic-uptime":{"sub_scores":[{"description":"It is evaluated whether the project is currently accessible. If it is not, this is usually due to technical reasons.","incidents":[],"weight":20,"status":"good","attributes":{"components_processed":1,"score":100,"limit":0,"tool":"KoalaPing","scoring":"good","components_failed":0},"score":100,"message":"All project pages can be reached and thus be visited by potential customers.","weighted_score":20,"title":"Current availability","identifier":"Score:Uptime:Down"},{"identifier":"Score:Uptime:Uptime","weighted_score":0,"message":"Unfortunately, we (and probably potential customers) could not reach some project pages in the past. There is an urgent need for action.","title":"Past availability","attributes":{"minValue":0,"limit":0,"score":0,"components_processed":1},"status":"bad","score":0,"description":"Here it is evaluated how often the on-line project was not attainable in the past 24 hours.","incidents":[],"weight":20}],"master":{"description":"If a project is unavailable, customers cannot visit, browse or make purchases. The accessibility assessment is based on the current situation, but also on historical values and trends.","incidents":[],"score":0,"attributes":{"score":0,"limit":0},"status":"bad","title":"Uptime","message":"Unfortunately, we (and probably potential customers) cannot reach some project pages. There is an urgent need for action.","identifier":"Score:Basic:Uptime"}},"basic-security":{"sub_scores":[{"weight":20,"incidents":[],"description":"In order for the web page content to be transmitted securely, web pages require a valid HTTPS certificate. Online stores without a certificate or with an expired certificate can be blocked by browsers.","status":"good","attributes":{"components_processed":1,"score":100,"tool":"HttpsCert","components_failed":0,"scoring":"good","limit":0},"score":100,"message":"The project's HTTPS certificates are still valid long enough and do not require renewal.","weighted_score":20,"title":"Certificate check","identifier":"Score:Security:Certificate"},{"identifier":"Score:Security:Cookiecount","weighted_score":20,"message":"The project handles cookies carefully and does not set too many.","title":"Cookie analysis","status":"good","attributes":{"components_processed":1,"score":100,"scoring":"good","tool":"CookieCount","components_failed":0,"limit":0},"score":100,"incidents":[],"description":"In order to guarantee data security for the customer and to be GDPR-compliant, cookies should be used sparingly. This evaluation is based on the number of cookies that are set.","weight":20},{"score":100,"attributes":{"components_failed":0,"tool":"insecurecontent","scoring":"good","limit":0,"components_processed":1,"score":100},"status":"good","incidents":[],"weight":20,"description":"Elements that are not transmitted in encrypted form are classified as unsafe by the browser. Such content must be removed from the site or switched to HTTPS.","identifier":"Score:Security:Insecurecontent","title":"Unsecure elements","weighted_score":20,"message":"No or only a few pages with unsafe elements were found."}],"master":{"score":100,"status":"good","attributes":{"limit":0,"score":100},"incidents":[],"description":"To give potential customers a trustworthy impression, it is important to treat user information as carefully as possible. Cookies should be used with caution and page contents should always be transmitted securely and encrypted.","identifier":"Score:Basic:Security","title":"Security","message":"Certificates and content on the website are up-to-date and secure. Browsers should classify the pages as secure."}},"basic-performance":{"master":{"incidents":[],"description":"The performance of a website is one of the most important characteristics that is relevant for both users and search engines. The performance rating is based on loading times, page size and the Google Lighthouse Score.","status":"good","attributes":{"limit":0,"score":100},"score":100,"message":"All performance parameters, such as loading speeds and page sizes, are in good range. Nothing needs to be changed here at the moment.","title":"Performance","identifier":"Score:Basic:Performance"},"sub_scores":[{"status":"good","attributes":{"components_processed":1,"score":100,"limit":0,"scoring":"good","tool":"performance","components_failed":0},"score":100,"incidents":[],"weight":20,"description":"Slowly responding servers give the project user poor quality and promote a higher bounce rate.","identifier":"Score:Performance:TimeToFirstByte","weighted_score":20,"message":"The server that delivers the project pages does this fast enough so that user do not have to wait long.","title":"Server speed"},{"identifier":"Score:Performance:TimeToLoad","message":"The project is displayed fast enough in the browser. This gives the user the impression of speed.","weighted_score":20,"title":"Browser speed","attributes":{"score":100,"components_processed":1,"tool":"performance","scoring":"good","components_failed":0,"limit":0},"status":"good","score":100,"description":"More and more logic is only executed in the user's browser. These logics, such as counting pixels, retargeting or custom store functions, can slow down the store considerably and reduce the perceived speed.","incidents":[],"weight":20},{"status":"good","attributes":{"components_processed":1,"score":100,"tool":"SiteInfo","components_failed":0,"scoring":"good","limit":0},"score":100,"incidents":[],"weight":20,"description":"Page size is one of the most important properties when it comes to loading times for web pages.","identifier":"Score:Performance:BigComponents","message":"The project pages have a good size and do not slow down the surfing of potential customers.","weighted_score":20,"title":"Page size"},{"message":"We have found no or very few large elements on the deposited project pages.","weighted_score":20,"title":"Large elements","identifier":"Score:Performance:BigResources","weight":20,"incidents":[],"description":"Large elements, such as oversized images, can slow down an online project considerably. Replacing them with optimized elements is often possible without any problems.","attributes":{"tool":"SiteInfo","components_failed":0,"scoring":"good","limit":0,"score":100,"components_processed":1},"status":"good","score":100},{"incidents":[],"weight":10,"description":"Google uses the in-house tool Google Lighthouse to evaluate the performance of websites and to make various optimization suggestions. This evaluation corresponds to Google value.","score":-1,"attributes":{"_scoring":"bad","score":-1,"limit":100},"status":"bad","title":"Google Performance Rating","weighted_score":0,"message":"This value is currently not calculated.","identifier":"Score:Performance:Lighthouse"}]}}}}|
    );
    my $verbose_expect = Cpanel::JSON::Load(
        q|[{"weight":20,"title":"Dead Links","identifier":"Score:Content:Deadlink","status":"good","attributes":{"tool":"DeadLink","score":100,"components_failed":0,"components_processed":1,"scoring":"good","limit":0},"weighted_score":20,"description":"Dead links, i.e. link to pages that no longer exist, lead to a negative user experience.","incidents":[],"message":"We have found few or no dead links on the monitored project pages.","score":100},{"message":"We have not found any faulty images, scripts or CSS files in the deposited store project.","incidents":[],"score":100,"weighted_score":20,"description":"Faulty elements, such as images that have been included but not found, can severely impact the shopping experience and reduce confidence in the project.","attributes":{"scoring":"good","components_processed":1,"limit":0,"score":100,"components_failed":0},"status":"good","title":"Broken resources","identifier":"Score:Content:Brokenresources","weight":20}]|
    );

    my $mock_locale = Test::MockModule->new('Cpanel::Locale::Utils::User');
    $mock_locale->redefine( 'get_user_locale' => sub { return 'en' } );

    my $proj = Cpanel::Koality::Project->new( api => $api, user => Cpanel::Koality::User->new( api => $api ) );

    require Clone;
    my $verbose_scores    = Clone::clone( $proj->get_all_scores(1) );
    my $nonverbose_scores = Clone::clone( $proj->get_all_scores(0) );

    is( $verbose_scores->[0]{scores}{'basic-content'}{sub_scores},    $verbose_expect, "Verbose output " )     or note Dumper $verbose_scores->[0]{scores}{'basic-content'};
    is( $nonverbose_scores->[0]{scores}{'basic-content'}{sub_scores}, [],              "Non-verbose output " ) or note Dumper $nonverbose_scores->[0]{scores}{'basic-content'};

    return;
}

sub test_get_project_by_id : Tests() {

    local $Cpanel::user = 'root';
    my $api = _setup_api_mock();

    $expected_api_response = {
        data => {
            projects => [
                {
                    id   => '1',
                    name => 'proj1'
                },
                {
                    id   => '2',
                    name => 'proj2'
                },
                {
                    id   => '3',
                    name => 'proj3'
                },
            ]
        }
    };

    my $mock_locale = Test::MockModule->new('Cpanel::Locale::Utils::User');
    $mock_locale->redefine( 'get_user_locale' => sub { return 'en' } );

    my $proj_obj = Cpanel::Koality::Project->new( api => $api, user => Cpanel::Koality::User->new( api => $api ) );

    my $proj = $proj_obj->get_project_by_id('1');

    is( $proj->{name}, $expected_api_response->{data}{projects}[0]{name}, "get_project_by_id: Got the correct data for project." );
    is( $proj->{id},   $expected_api_response->{data}{projects}[0]{id},   "get_project_by_id: Got the correct data for project." );

    return;
}

sub test_get_system_size_id : Tests(2) {

    local $Cpanel::user = 'root';
    my $api = _setup_api_mock();

    my $mock_locale = Test::MockModule->new('Cpanel::Locale::Utils::User');
    $mock_locale->redefine( 'get_user_locale' => sub { return 'en' } );

    my $proj = Cpanel::Koality::Project->new( api => $api, user => Cpanel::Koality::User->new( api => $api ) );
    $proj->user->user_config( { full => { user => { company => { id => '1234' } } } } );
    $expected_api_response = { data => { systems_new => { pro => { id => 'abcd' } } } };

    ok( dies { $proj->get_system_size_id('doesNotExist') }, "get_system_size_by_id: Dies if the type doesn't exist." );

    is( $proj->get_system_size_id('pro'), 'abcd', "get_system_type_by_id: Got expected system id for the specified type." );

    return;
}

sub _test_create_project : Tests(2) {

    local $Cpanel::user = 'root';
    my $api = _setup_api_mock();

    my $expected_system_type_id = '1';
    my $expected_system_size_id = '2';
    my $mock_system_type        = Test::MockModule->new('Cpanel::Koality::Project');
    $mock_system_type->redefine( get_system_id_for_type => sub { return $expected_system_type_id } );
    $mock_system_type->redefine( get_system_size_id     => sub { return $expected_system_size_id } );
    $mock_system_type->redefine( get_project_by_id      => sub { return '1234' } );

    $expected_api_response = { data => { project => { id => '1234' } } };

    my $mock_locale = Test::MockModule->new('Cpanel::Locale::Utils::User');
    $mock_locale->redefine( 'get_user_locale' => sub { return 'en' } );

    my $proj = Cpanel::Koality::Project->new( api => $api, user => Cpanel::Koality::User->new( api => $api ) );
    ok( dies { $proj->create_project() }, "create_project: Dies when no url or name specified." );
    is( $proj->create_project( { name => 'myProj', url => 'https://myproj.com' } ), '1234', "create_project: Got expected project id." );

    return;
}

sub _test_create_component : Tests(2) {

    local $Cpanel::user = 'root';
    my $api = _setup_api_mock();

    my $expected_comp_type = {
        html => {
            identifier  => 'html',
            description => '',
            id          => 1,
            name        => 'HTML Document'
        }
    };

    my $mock_comp_type = Test::MockModule->new('Cpanel::Koality::Project');

    $mock_comp_type->redefine( get_component_types => sub { return $expected_comp_type } );

    $expected_api_response = { data => { component => { id => '9999' } } };

    my $mock_locale = Test::MockModule->new('Cpanel::Locale::Utils::User');
    $mock_locale->redefine( 'get_user_locale' => sub { return 'en' } );

    my $proj = Cpanel::Koality::Project->new( api => $api, user => Cpanel::Koality::User->new( api => $api ) );
    ok( dies { $proj->create_component() }, "create_component: Dies when required arguments are not passed." );
    is( $proj->create_component( { projectid => '1234', systemid => '5678', domain => 'https://myproj.com/ok' } ), $expected_api_response, "create_component: Got expected component." );

    return;
}

sub get_component_types : Tests(2) {

    local $Cpanel::user = 'root';
    my $api = _setup_api_mock();

    my $mock_locale = Test::MockModule->new('Cpanel::Locale::Utils::User');
    $mock_locale->redefine( 'get_user_locale' => sub { return 'en' } );

    my $proj = Cpanel::Koality::Project->new( api => $api, user => Cpanel::Koality::User->new( api => $api ) );

    $expected_api_response = {
        data => {
            system_types => [
                {
                    id          => 1,
                    identifier  => 'html',
                    description => '',
                    name        => 'HTML Document'
                },
            ]
        }
    };

    my $expected_comp_type = {
        html => {
            identifier  => 'html',
            description => '',
            id          => 1,
            name        => 'HTML Document'
        }
    };

    ok( dies { $proj->get_component_types() }, "get_component_types: Dies if required args are not passed." );
    is( $proj->get_component_types('1234'), $expected_comp_type, "get_component_types: Got expected component types." );

    return;
}

sub test_build_location : Tests(7) {

    local $Cpanel::user = 'root';
    my $api = _setup_api_mock();

    my $cc;
    my $geo_mock = Test::MockModule->new('Cpanel::GeoIPfree');
    $geo_mock->redefine( 'new'    => sub { return bless {}, shift } );
    $geo_mock->redefine( 'Faster' => sub { return 1 } );
    $geo_mock->redefine( 'LookUp' => sub { return $cc } );

    my $cc_map = {
        US => 'NA',
        BR => 'SA',
        DE => 'EU',
        FR => 'EU',
        JP => 'AS',
        AU => 'OC',
    };

    my $map_mock = Test::MockModule->new('Cpanel::JSON');
    $map_mock->redefine( 'LoadFile' => sub { return $cc_map } );

    my $url  = 'www.domain.com';
    my $proj = Cpanel::Koality::Project->new( 'url' => $url, user => Cpanel::Koality::User->new( api => $api ) );

    my @tests = (
        { cc => 'NA', result => 'us_east' },
        { cc => 'BR', result => 'us_east' },
        { cc => 'DE', result => 'de' },
        { cc => 'FR', result => 'de' },
        { cc => 'JP', result => 'asia_jp' },
        { cc => 'AU', result => 'asia_jp' },
    );

    foreach my $test (@tests) {
        $cc = $test->{cc};
        is( $proj->_build_location(), $test->{result}, "Got expected geo location derived from the country code $cc." );
    }

    $cc = 'NA';
    $proj->use_stage(1);
    is( $proj->_build_location(), 'de', "Got de when using the staging backend." );

    return;
}

sub _setup_api_mock {

    my $api = Cpanel::Koality::ApiClient->new( 'http_client' => sub { } );
    $api = Test::MockObject::Extends->new($api);

    $api->mock( 'run'         => sub { return $expected_api_response } );
    $api->mock( 'http_client' => sub { return 1 } );

    return $api;
}

1;
