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

#                                      Copyright 2025 WebPros International, LLC
#                                                           All rights reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited.

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

use Test2::V0;
use Test2::Tools::Explain;
use Test2::Tools::Compare;
use Test2::Tools::Exception;
use Test2::Plugin::NoWarnings;

use Test::MockModule;
use Test::MockFile;
use Cpanel::JSON;

# Pre-load stub modules to prevent dependencies from failing
BEGIN {
    $INC{'Cpanel/Plugins/Components/Rules.pm'} = 1;
    $INC{'Cpanel/Plugins/Components/Rules/Util/NVData.pm'} = 1;
    $INC{'Cpanel/Plugins/Components/Rules/CompanyId.pm'} = 1;
    
    # Pre-load Cpanel modules
    $INC{'Cpanel/Server/Type.pm'} = 1;
    package Cpanel::Server::Type;
    sub get_products { return { 'cometbackup' => 1 }; }
    
    $INC{'Cpanel/PwCache.pm'} = 1;
    package Cpanel::PwCache;
    sub new { return bless {}, shift; }

    $INC{'Cpanel/Config/Users.pm'} = 1;
    package Cpanel::Config::Users;
    sub getcpusers { return qw(user1 user2); }
    
    # Pre-load CometBackup modules with stubs that can be overridden
    $INC{'Whostmgr/CometBackup/Setup.pm'} = 1;
    package Whostmgr::CometBackup::Setup;
    our @EXPORT = qw(is_comet_agent_installed is_agent_logged_in setup_comet_agent install_comet_agent);
    sub is_comet_agent_installed { return 0; }
    sub is_agent_logged_in { return 0; }
    sub setup_comet_agent { return 1; }
    sub install_comet_agent { return 1; }
    
    $INC{'Whostmgr/CometBackup/AgentManager.pm'} = 1;
    package Whostmgr::CometBackup::AgentManager;
    our @EXPORT = qw(get_status start);
    sub get_status { return (1, 'active'); }
    sub start { return (1, ''); }
    
    $INC{'Whostmgr/CometBackup/CometBackupAPI.pm'} = 1;
    package Whostmgr::CometBackup::CometBackupAPI;
    our @EXPORT = qw(get_server_config);
    sub get_server_config { return {}; }
    
    $INC{'Whostmgr/CometBackup/BackupRunLogs.pm'} = 1;
    package Whostmgr::CometBackup::BackupRunLogs;
    sub new { return bless {}, shift; }

    $INC{'Whostmgr/CometBackup/BackupJobs.pm'} = 1;
    package Whostmgr::CometBackup::BackupJobs;
    sub new { return bless {}, shift; }
    sub create_backup_job { return 'test-job-uuid'; }
    
    $INC{'Whostmgr/CometBackup/ProtectedItemMap.pm'} = 1;
    package Whostmgr::CometBackup::ProtectedItemMap;
    sub new { return bless {}, shift; }
    sub create_protected_items_for_user_job { return ('item-uid-1', 'item-uid-2'); }
    sub get_protected_items_for_user { 
        return (
            { protected_item_uid => 'item-uid-1', item_type => 'homedir', item_configuration => {} },
            { protected_item_uid => 'item-uid-2', item_type => 'databases', item_configuration => {} },
        );
    }
    sub mark_items_synced_to_comet { return 1; }
    
    $INC{'Whostmgr/CometBackup/CometBackupAPI/ProtectedItemBatch.pm'} = 1;
    package Whostmgr::CometBackup::CometBackupAPI::ProtectedItemBatch;
    sub new { 
        my ($class, %args) = @_;
        return bless { target_user => $args{target_user} }, $class;
    }
    sub add_protected_item { return 1; }
    sub submit { return 1; }
    sub _get_current_user_hash { return 'test-hash'; }
    sub _get_user_profile { return { ProfileHash => 'test-hash' }; }
}

# Mock the Constants module BEFORE importing CometBackup
BEGIN {
    $INC{'Whostmgr/CometBackup/Constants.pm'} = 1;
    package Whostmgr::CometBackup::Constants;
    our $DISABLED_TOUCHFILE = '/var/cpanel/comet/comet_backup_disabled';
    our $BACKUP_RUNNER_PATH = '/usr/local/cpanel/backup-runner.pl';
    our $RUN_LOG_DB = '/var/cpanel/comet/backup_run_logs.db';
    1;
}

use Whostmgr::API::1::CometBackup ();

# Mock the logger globally to prevent file access
my $mock_logger_global = Test::MockModule->new('Cpanel::Logger');
$mock_logger_global->redefine(
    'info' => sub { return 1; },
    'error' => sub { return 1; },
    'warn' => sub { return 1; },
    'debug' => sub { return 1; }
);

subtest 'create_backup_job - missing JSON parameter' => sub {
    my $mock_utils = Test::MockModule->new('Whostmgr::API::1::Utils');
    $mock_utils->redefine(
        'set_metadata_not_ok' => sub {
            my ( $metadata, $error ) = @_;
            $metadata->{ok} = 0;
            $metadata->{error} = $error;
            return 1;
        }
    );
    
    my $args = {};
    my $metadata = {};
    
    my $response = Whostmgr::API::1::CometBackup::create_backup_job( $args, $metadata );
    
    is $metadata->{ok}, 0, 'Metadata is set to not ok';
    like $metadata->{error}, qr/Missing required 'json'/i, 'Error mentions missing json parameter';
    
    return;
};

subtest 'create_backup_job - missing job_config' => sub {
    my $mock_utils = Test::MockModule->new('Whostmgr::API::1::Utils');
    $mock_utils->redefine(
        'set_metadata_not_ok' => sub {
            my ( $metadata, $error ) = @_;
            $metadata->{ok} = 0;
            $metadata->{error} = $error;
            return 1;
        }
    );
    
    my $args = { json => Cpanel::JSON::Dump( { other_field => 'value' } ) };
    my $metadata = {};
    
    my $response = Whostmgr::API::1::CometBackup::create_backup_job( $args, $metadata );
    
    is $metadata->{ok}, 0, 'Metadata is set to not ok';
    like $metadata->{error}, qr/job_config.*required/i, 'Error mentions job_config required';
    
    return;
};

subtest 'create_backup_job - job_config not a hash' => sub {
    my $mock_utils = Test::MockModule->new('Whostmgr::API::1::Utils');
    $mock_utils->redefine(
        'set_metadata_not_ok' => sub {
            my ( $metadata, $error ) = @_;
            $metadata->{ok} = 0;
            $metadata->{error} = $error;
            return 1;
        }
    );
    
    my $args = { json => Cpanel::JSON::Dump( { job_config => 'string instead of hash' } ) };
    my $metadata = {};
    
    my $response = Whostmgr::API::1::CometBackup::create_backup_job( $args, $metadata );
    
    is $metadata->{ok}, 0, 'Metadata is set to not ok';
    like $metadata->{error}, qr/job_config.*hash reference/i, 'Error mentions hash reference';
    
    return;
};

subtest 'create_backup_job - missing required field (name)' => sub {
    my $mock_utils = Test::MockModule->new('Whostmgr::API::1::Utils');
    $mock_utils->redefine(
        'set_metadata_not_ok' => sub {
            my ( $metadata, $error ) = @_;
            $metadata->{ok} = 0;
            $metadata->{error} = $error;
            return 1;
        }
    );
    
    my $args = { json => Cpanel::JSON::Dump( { 
        job_config => {
            schedule_type => 'daily',
            backup_options => {},
            destinations => [],
            all_accounts => 1,
        }
    } ) };
    my $metadata = {};
    
    my $response = Whostmgr::API::1::CometBackup::create_backup_job( $args, $metadata );
    
    is $metadata->{ok}, 0, 'Metadata is set to not ok';
    like $metadata->{error}, qr/job_config\.name.*required/i, 'Error mentions name field';
    
    return;
};

subtest 'create_backup_job - missing required field (schedule_type)' => sub {
    my $mock_utils = Test::MockModule->new('Whostmgr::API::1::Utils');
    $mock_utils->redefine(
        'set_metadata_not_ok' => sub {
            my ( $metadata, $error ) = @_;
            $metadata->{ok} = 0;
            $metadata->{error} = $error;
            return 1;
        }
    );
    
    my $args = { json => Cpanel::JSON::Dump( { 
        job_config => {
            name => 'My Backup',
            backup_options => {},
            destinations => [],
            all_accounts => 1,
        }
    } ) };
    my $metadata = {};
    
    my $response = Whostmgr::API::1::CometBackup::create_backup_job( $args, $metadata );
    
    is $metadata->{ok}, 0, 'Metadata is set to not ok';
    like $metadata->{error}, qr/job_config\.schedule_type.*required/i, 'Error mentions schedule_type field';
    
    return;
};

subtest 'create_backup_job - missing required field (backup_options)' => sub {
    my $mock_utils = Test::MockModule->new('Whostmgr::API::1::Utils');
    $mock_utils->redefine(
        'set_metadata_not_ok' => sub {
            my ( $metadata, $error ) = @_;
            $metadata->{ok} = 0;
            $metadata->{error} = $error;
            return 1;
        }
    );
    
    my $args = { json => Cpanel::JSON::Dump( { 
        job_config => {
            name => 'My Backup',
            schedule_type => 'daily',
            destinations => [],
            all_accounts => 1,
        }
    } ) };
    my $metadata = {};
    
    my $response = Whostmgr::API::1::CometBackup::create_backup_job( $args, $metadata );
    
    is $metadata->{ok}, 0, 'Metadata is set to not ok';
    like $metadata->{error}, qr/job_config\.backup_options.*required/i, 'Error mentions backup_options field';
    
    return;
};

subtest 'create_backup_job - missing required field (destinations)' => sub {
    my $mock_utils = Test::MockModule->new('Whostmgr::API::1::Utils');
    $mock_utils->redefine(
        'set_metadata_not_ok' => sub {
            my ( $metadata, $error ) = @_;
            $metadata->{ok} = 0;
            $metadata->{error} = $error;
            return 1;
        }
    );
    
    my $args = { json => Cpanel::JSON::Dump( { 
        job_config => {
            name => 'My Backup',
            schedule_type => 'daily',
            backup_options => {},
            all_accounts => 1,
        }
    } ) };
    my $metadata = {};
    
    my $response = Whostmgr::API::1::CometBackup::create_backup_job( $args, $metadata );
    
    is $metadata->{ok}, 0, 'Metadata is set to not ok';
    like $metadata->{error}, qr/job_config\.destinations.*required/i, 'Error mentions destinations field';
    
    return;
};

subtest 'create_backup_job - missing required field (all_accounts)' => sub {
    my $mock_utils = Test::MockModule->new('Whostmgr::API::1::Utils');
    $mock_utils->redefine(
        'set_metadata_not_ok' => sub {
            my ( $metadata, $error ) = @_;
            $metadata->{ok} = 0;
            $metadata->{error} = $error;
            return 1;
        }
    );
    
    my $args = { json => Cpanel::JSON::Dump( { 
        job_config => {
            name => 'My Backup',
            schedule_type => 'daily',
            backup_options => {},
            destinations => [],
        }
    } ) };
    my $metadata = {};
    
    my $response = Whostmgr::API::1::CometBackup::create_backup_job( $args, $metadata );
    
    is $metadata->{ok}, 0, 'Metadata is set to not ok';
    like $metadata->{error}, qr/job_config\.all_accounts.*required/i, 'Error mentions all_accounts field';
    
    return;
};

subtest 'create_backup_job - all_accounts false but no accounts array' => sub {
    my $mock_utils = Test::MockModule->new('Whostmgr::API::1::Utils');
    $mock_utils->redefine(
        'set_metadata_not_ok' => sub {
            my ( $metadata, $error ) = @_;
            $metadata->{ok} = 0;
            $metadata->{error} = $error;
            return 1;
        }
    );
    
    my $args = { json => Cpanel::JSON::Dump( { 
        job_config => {
            name => 'My Backup',
            schedule_type => 'daily',
            backup_options => {},
            destinations => [],
            all_accounts => 0,
        }
    } ) };
    my $metadata = {};
    
    my $response = Whostmgr::API::1::CometBackup::create_backup_job( $args, $metadata );
    
    is $metadata->{ok}, 0, 'Metadata is set to not ok';
    like $metadata->{error}, qr/accounts.*non-empty array/i, 'Error mentions accounts array required';
    
    return;
};

subtest 'create_backup_job - all_accounts false with empty accounts array' => sub {
    my $mock_utils = Test::MockModule->new('Whostmgr::API::1::Utils');
    $mock_utils->redefine(
        'set_metadata_not_ok' => sub {
            my ( $metadata, $error ) = @_;
            $metadata->{ok} = 0;
            $metadata->{error} = $error;
            return 1;
        }
    );
    
    my $args = { json => Cpanel::JSON::Dump( { 
        job_config => {
            name => 'My Backup',
            schedule_type => 'daily',
            backup_options => {},
            destinations => [],
            all_accounts => 0,
            accounts => [],
        }
    } ) };
    my $metadata = {};
    
    my $response = Whostmgr::API::1::CometBackup::create_backup_job( $args, $metadata );
    
    is $metadata->{ok}, 0, 'Metadata is set to not ok';
    like $metadata->{error}, qr/accounts.*non-empty array/i, 'Error mentions non-empty array required';
    
    return;
};

subtest 'create_backup_job - server config error' => sub {
    my $mock_utils = Test::MockModule->new('Whostmgr::API::1::Utils');
    $mock_utils->redefine(
        'set_metadata_not_ok' => sub {
            my ( $metadata, $error ) = @_;
            $metadata->{ok} = 0;
            $metadata->{error} = $error;
            return 1;
        }
    );
    
    my $mock_api = Test::MockModule->new('Whostmgr::CometBackup::CometBackupAPI');
    $mock_api->redefine(
        'get_server_config' => sub {
            die "Connection failed";
        }
    );
    
    my $args = { json => Cpanel::JSON::Dump( { 
        job_config => {
            name => 'My Backup',
            schedule_type => 'daily',
            backup_options => {},
            destinations => [],
            all_accounts => 1,
        }
    } ) };
    my $metadata = {};
    
    my $response = Whostmgr::API::1::CometBackup::create_backup_job( $args, $metadata );
    
    is $metadata->{ok}, 0, 'Metadata is set to not ok';
    like $metadata->{error}, qr/server not configured/i, 'Error mentions server not configured';
    
    return;
};

subtest 'create_backup_job - create_backup_job fails' => sub {
    my $mock_utils = Test::MockModule->new('Whostmgr::API::1::Utils');
    $mock_utils->redefine(
        'set_metadata_not_ok' => sub {
            my ( $metadata, $error ) = @_;
            $metadata->{ok} = 0;
            $metadata->{error} = $error;
            return 1;
        }
    );
    
    my $mock_api = Test::MockModule->new('Whostmgr::CometBackup::CometBackupAPI');
    $mock_api->redefine(
        'get_server_config' => sub {
            return { username => 'testuser' };
        }
    );
    
    my $mock_jobs = Test::MockModule->new('Whostmgr::CometBackup::BackupJobs');
    $mock_jobs->redefine(
        'create_backup_job' => sub {
            die "Failed to create backup job";
        }
    );
    
    my $args = { json => Cpanel::JSON::Dump( { 
        job_config => {
            name => 'My Backup',
            schedule_type => 'daily',
            backup_options => {},
            destinations => [],
            all_accounts => 1,
        }
    } ) };
    my $metadata = {};
    
    my $response = Whostmgr::API::1::CometBackup::create_backup_job( $args, $metadata );
    
    is $metadata->{ok}, 0, 'Metadata is set to not ok';
    like $metadata->{error}, qr/Failed to create backup job/i, 'Error mentions backup job creation failed';
    
    return;
};

subtest 'create_backup_job - successful with all_accounts' => sub {
    my $mock_utils = Test::MockModule->new('Whostmgr::API::1::Utils');
    $mock_utils->redefine(
        'set_metadata_ok' => sub {
            my ($metadata) = @_;
            $metadata->{ok} = 1;
            return 1;
        },
        'set_metadata_not_ok' => sub {
            my ( $metadata, $error ) = @_;
            $metadata->{ok} = 0;
            $metadata->{error} = $error;
            return 1;
        }
    );
    
    my $mock_api = Test::MockModule->new('Whostmgr::CometBackup::CometBackupAPI');
    $mock_api->redefine(
        'get_server_config' => sub {
            return { username => 'testuser' };
        }
    );
    
    my $args = { json => Cpanel::JSON::Dump( { 
        job_config => {
            name => 'My Backup',
            schedule_type => 'daily',
            backup_options => {},
            destinations => [],
            all_accounts => 1,
        }
    } ) };
    my $metadata = {};
    
    my $response = Whostmgr::API::1::CometBackup::create_backup_job( $args, $metadata );
    
    is $metadata->{ok}, 1, 'Metadata is set to ok';
    ok exists $response->{job}, 'Response has job key';
    ok exists $response->{job}->{job_uuid}, 'Response has job_uuid';
    is $response->{job}->{job_uuid}, 'test-job-uuid', 'Job UUID is returned';
    
    return;
};

subtest 'create_backup_job - successful with specific accounts' => sub {
    my $mock_utils = Test::MockModule->new('Whostmgr::API::1::Utils');
    $mock_utils->redefine(
        'set_metadata_ok' => sub {
            my ($metadata) = @_;
            $metadata->{ok} = 1;
            return 1;
        },
        'set_metadata_not_ok' => sub {
            my ( $metadata, $error ) = @_;
            $metadata->{ok} = 0;
            $metadata->{error} = $error;
            return 1;
        }
    );
    
    my $mock_api = Test::MockModule->new('Whostmgr::CometBackup::CometBackupAPI');
    $mock_api->redefine(
        'get_server_config' => sub {
            return { username => 'testuser' };
        }
    );
    
    my $args = { json => Cpanel::JSON::Dump( { 
        job_config => {
            name => 'My Backup',
            schedule_type => 'daily',
            backup_options => {},
            destinations => [],
            all_accounts => 0,
            accounts => [ 'user1', 'user2' ],
        }
    } ) };
    my $metadata = {};
    
    my $response = Whostmgr::API::1::CometBackup::create_backup_job( $args, $metadata );
    
    is $metadata->{ok}, 1, 'Metadata is set to ok';
    ok exists $response->{job}, 'Response has job key';
    ok exists $response->{job}->{job_uuid}, 'Response has job_uuid';
    is $response->{job}->{job_uuid}, 'test-job-uuid', 'Job UUID is returned';
    
    return;
};

subtest 'create_backup_job - run_now triggers immediate backup' => sub {
    my $mock_utils = Test::MockModule->new('Whostmgr::API::1::Utils');
    $mock_utils->redefine(
        'set_metadata_ok' => sub {
            my ($metadata) = @_;
            $metadata->{ok} = 1;
            return 1;
        },
        'set_metadata_not_ok' => sub {
            my ( $metadata, $error ) = @_;
            $metadata->{ok} = 0;
            $metadata->{error} = $error;
            return 1;
        }
    );
    
    my $mock_api = Test::MockModule->new('Whostmgr::CometBackup::CometBackupAPI');
    $mock_api->redefine(
        'get_server_config' => sub {
            return { username => 'testuser' };
        }
    );
    
    my $run_backup_called = 0;
    my $mock_cometbackup = Test::MockModule->new('Whostmgr::API::1::CometBackup');
    $mock_cometbackup->redefine(
        'run_backup_job' => sub {
            $run_backup_called = 1;
            return { started => 1 };
        }
    );
    
    my $args = { json => Cpanel::JSON::Dump( { 
        job_config => {
            name => 'My Backup',
            schedule_type => 'daily',
            backup_options => {},
            destinations => [],
            all_accounts => 1,
            run_now => 1,
        }
    } ) };
    my $metadata = {};
    
    my $response = Whostmgr::API::1::CometBackup::create_backup_job( $args, $metadata );
    
    is $metadata->{ok}, 1, 'Metadata is set to ok';
    ok $run_backup_called, 'run_backup_job was called due to run_now flag';
    
    return;
};

subtest 'create_backup_job - run_now failure does not fail entire operation' => sub {
    my $mock_utils = Test::MockModule->new('Whostmgr::API::1::Utils');
    $mock_utils->redefine(
        'set_metadata_ok' => sub {
            my ($metadata) = @_;
            $metadata->{ok} = 1;
            return 1;
        },
        'set_metadata_not_ok' => sub {
            my ( $metadata, $error ) = @_;
            $metadata->{ok} = 0;
            $metadata->{error} = $error;
            return 1;
        }
    );
    
    my $mock_api = Test::MockModule->new('Whostmgr::CometBackup::CometBackupAPI');
    $mock_api->redefine(
        'get_server_config' => sub {
            return { username => 'testuser' };
        }
    );
    
    my $mock_cometbackup = Test::MockModule->new('Whostmgr::API::1::CometBackup');
    $mock_cometbackup->redefine(
        'run_backup_job' => sub {
            die "Failed to run backup";
        }
    );
    
    my $args = { json => Cpanel::JSON::Dump( { 
        job_config => {
            name => 'My Backup',
            schedule_type => 'daily',
            backup_options => {},
            destinations => [],
            all_accounts => 1,
            run_now => 1,
        }
    } ) };
    my $metadata = {};
    
    my $response = Whostmgr::API::1::CometBackup::create_backup_job( $args, $metadata );
    
    is $metadata->{ok}, 1, 'Metadata is still ok even though run_now failed';
    ok exists $response->{job}, 'Response still has job key';
    is $response->{job}->{job_uuid}, 'test-job-uuid', 'Job was still created despite run_now error';
    
    return;
};

subtest 'create_backup_job - batch submit with non-hash-mismatch error' => sub {
    my $mock_utils = Test::MockModule->new('Whostmgr::API::1::Utils');
    $mock_utils->redefine(
        'set_metadata_ok' => sub {
            my ($metadata) = @_;
            $metadata->{ok} = 1;
            return 1;
        },
        'set_metadata_not_ok' => sub {
            my ( $metadata, $error ) = @_;
            $metadata->{ok} = 0;
            $metadata->{error} = $error;
            return 1;
        }
    );
    
    my $mock_api = Test::MockModule->new('Whostmgr::CometBackup::CometBackupAPI');
    $mock_api->redefine(
        'get_server_config' => sub {
            return { username => 'testuser' };
        }
    );
    
    # When we create a ProtectedItemBatch::new, it will have the default submit 
    # that returns 1, which will lead to a successful job creation
    
    my $args = { json => Cpanel::JSON::Dump( { 
        job_config => {
            name => 'My Backup',
            schedule_type => 'daily',
            backup_options => {},
            destinations => [],
            all_accounts => 1,
        }
    } ) };
    my $metadata = {};
    
    my $response = Whostmgr::API::1::CometBackup::create_backup_job( $args, $metadata );
    
    is $metadata->{ok}, 1, 'Successful batch submission returns ok';
    ok defined $response, 'Response is defined';
    ok exists $response->{job}, 'Response has job key on successful submission';
    
    return;
};

done_testing();
