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

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 File::Temp;

# Mock the constants before loading the modules
BEGIN {
    $INC{'Whostmgr/CometBackup/Constants.pm'} = 1;

    package Whostmgr::CometBackup::Constants;
    our $RUN_LOG_DB            = '/tmp/comet_test_run_logs.db';
    our $PROTECTED_ITEM_MAP_DB = '/tmp/comet_test_map.db';
}

# Mock files that might be accessed
my @mock_files = (
    Test::MockFile->file( '/usr/local/cpanel/logs/error_log', '' ),
    Test::MockFile->file( '/var/cpanel/server_locale',        '' ),
    Test::MockFile->dir('/var'),
    Test::MockFile->dir('/var/log'),
    Test::MockFile->file( '/var/log/comet_backup.log', '' ),
    Test::MockFile->dir('/var/cpanel'),
    Test::MockFile->dir('/var/cpanel/comet'),
    Test::MockFile->file( '/tmp/comet_test_run_logs.db', '' ),
    Test::MockFile->file( '/tmp/comet_test_map.db',      '' ),
);

use Whostmgr::CometBackup::DeletedUsers ();

subtest 'new' => sub {
    my $du = Whostmgr::CometBackup::DeletedUsers->new();
    ok $du, 'Created DeletedUsers object';
    isa_ok $du, 'Whostmgr::CometBackup::DeletedUsers';
};

subtest 'record_deleted_user' => sub {
    my ( $du, $mock_map, $mock_logger ) = setup();

    my $recorded_user;
    my $recorded_usage;
    $mock_map->redefine(
        'mark_user_deleted' => sub {
            my ( $self, %args ) = @_;
            $recorded_user  = $args{system_user};
            $recorded_usage = $args{estimated_usage_bytes};
            return 1;    # Simulate successful record
        }
    );

    my $result = $du->record_deleted_user(
        system_user           => 'testuser',
        estimated_usage_bytes => 5_000_000_000
    );

    is $result,         1,             'record_deleted_user returns 1 on success';
    is $recorded_user,  'testuser',    'User was recorded correctly';
    is $recorded_usage, 5_000_000_000, 'Usage was recorded correctly';
};

subtest 'record_deleted_user - missing system_user' => sub {
    my ( $du, $mock_map, $mock_logger ) = setup();

    like(
        dies { $du->record_deleted_user( estimated_usage_bytes => 1000 ) },
        qr/system_user parameter required/,
        'Dies when system_user is missing'
    );
};

subtest 'record_deleted_user - no protected items warning' => sub {
    my ( $du, $mock_map, $mock_logger ) = setup();

    $mock_map->redefine(
        'mark_user_deleted' => sub {
            return 0;    # No protected items found
        }
    );

    my $result = $du->record_deleted_user( system_user => 'testuser' );
    is $result, 1, 'Returns success even when no protected items exist';
};

subtest 'get_deleted_user' => sub {
    my ( $du, $mock_map, $mock_logger ) = setup();

    $mock_map->redefine(
        'get_deleted_user' => sub {
            my ( $self, $username ) = @_;
            return {
                system_user           => $username,
                estimated_usage_bytes => 10_000_000,
                deleted_at            => time(),
            };
        }
    );

    my $info = $du->get_deleted_user('testuser');
    is $info->{system_user},           'testuser', 'Retrieved user info correctly';
    is $info->{estimated_usage_bytes}, 10_000_000, 'Usage info correct';
};

subtest 'get_deleted_user - missing parameter' => sub {
    my ( $du, $mock_map, $mock_logger ) = setup();

    like(
        dies { $du->get_deleted_user(undef) },
        qr/system_user parameter required/,
        'Dies when system_user is missing'
    );
};

subtest 'list_deleted_users' => sub {
    my ( $du, $mock_map, $mock_logger ) = setup();

    $mock_map->redefine(
        'get_deleted_users' => sub {
            return (
                { system_user => 'user1', estimated_usage_bytes => 1000 },
                { system_user => 'user2', estimated_usage_bytes => 2000 },
            );
        }
    );

    my @users = $du->list_deleted_users();
    is scalar(@users),           2,       'Returns correct number of users';
    is $users[0]->{system_user}, 'user1', 'First user correct';
    is $users[1]->{system_user}, 'user2', 'Second user correct';
};

subtest 'mark_pruned' => sub {
    my ( $du, $mock_map, $mock_logger ) = setup();

    my $pruned_user;
    my $flag_value;
    $mock_map->redefine(
        'set_deleted_user_pruned_flag' => sub {
            my ( $self, %args ) = @_;
            $pruned_user = $args{system_user};
            $flag_value  = $args{flag};
            return 1;
        }
    );

    my $result = $du->mark_pruned('testuser');
    is $result,      1,          'Returns success';
    is $pruned_user, 'testuser', 'User was marked pruned';
    is $flag_value,  1,          'Flag set to 1';
};

subtest 'list_deleted_user_snapshots - not tracked' => sub {
    my ( $du, $mock_map, $mock_logger ) = setup();

    $mock_map->redefine(
        'get_deleted_user' => sub {
            return undef;
        }
    );

    my $result = $du->list_deleted_user_snapshots('testuser');
    ok $result->{error}, 'Returns error when user not tracked';
    like $result->{error}, qr/not tracked in deleted user metadata/, 'Error message correct';
};

subtest 'list_deleted_user_snapshots - database not found' => sub {
    my ( $du, $mock_map, $mock_logger, $mock_api ) = setup();

    $mock_map->redefine(
        'get_deleted_user' => sub {
            return { system_user => 'testuser' };
        }
    );

    $mock_api->redefine(
        'get_server_config' => sub {
            return { server_url => 'http://localhost', username => 'admin', password => 'pass' };
        }
    );

    # The mock file doesn't really exist for -f check
    my $result = $du->list_deleted_user_snapshots('testuser');
    ok $result->{error}, 'Returns error when database not found or query fails';
    like $result->{error}, qr/Backup run logs|Failed to query/, 'Error message contains expected text';
    is $result->{count},            0,       'Count is 0';
    is ref( $result->{snapshots} ), 'ARRAY', 'Snapshots is an array';
};

subtest 'list_deleted_user_snapshots - with mock database' => sub {
    my ( $du, $mock_map, $mock_logger, $mock_api, $mock_dbi ) = setup();

    $mock_map->redefine(
        'get_deleted_user' => sub {
            return { system_user => 'testuser' };
        }
    );

    $mock_api->redefine(
        'get_server_config' => sub {
            return { server_url => 'http://localhost', username => 'admin', password => 'pass' };
        }
    );

    # Create a real temp file so -f check passes
    my $temp_db   = File::Temp->new( UNLINK => 1, SUFFIX => '.db' );
    my $temp_path = $temp_db->filename;

    # Mock the constant to point to temp file
    no warnings 'redefine';
    local $Whostmgr::CometBackup::Constants::RUN_LOG_DB = $temp_path;

    # Mock DBI to return empty results
    $mock_dbi->redefine(
        'connect' => sub {
            my $mock_sth = bless {}, 'MockSTHEmpty';
            return bless { _mock_sth => $mock_sth }, 'MockDBH';
        }
    );

    my $result = $du->list_deleted_user_snapshots('testuser');
    ok !$result->{error}, 'No error with valid database';
    is $result->{username},         'testuser', 'Username correct';
    is $result->{count},            0,          'Count is 0 for no snapshots';
    is ref( $result->{snapshots} ), 'ARRAY',    'Snapshots is an array';
};

subtest 'get_user_usage' => sub {
    my ( $du, $mock_map, $mock_logger ) = setup();

    $mock_map->redefine(
        'get_deleted_user' => sub {
            return { estimated_usage_bytes => 123456789 };
        }
    );

    my $usage = $du->get_user_usage('testuser');
    is $usage, 123456789, 'Returns correct usage';
};

subtest 'get_user_usage - user not found' => sub {
    my ( $du, $mock_map, $mock_logger ) = setup();

    $mock_map->redefine(
        'get_deleted_user' => sub {
            return undef;
        }
    );

    my $usage = $du->get_user_usage('testuser');
    is $usage, 0, 'Returns 0 when user not found';
};

subtest 'prune_deleted_users_before' => sub {
    my ( $du, $mock_map, $mock_logger ) = setup();

    my $purge_cutoff;
    $mock_map->redefine(
        'purge_deleted_users_before_epoch' => sub {
            my ( $self, $cutoff ) = @_;
            $purge_cutoff = $cutoff;
            return 5;    # Simulate 5 users purged
        }
    );

    my $cutoff = time() - 30 * 86400;
    my $count  = $du->prune_deleted_users_before( cutoff_epoch => $cutoff );

    is $count,        5,       'Returns correct purge count';
    is $purge_cutoff, $cutoff, 'Cutoff passed correctly';
};

subtest 'prune_deleted_users_before - missing parameter' => sub {
    my ( $du, $mock_map, $mock_logger ) = setup();

    like(
        dies { $du->prune_deleted_users_before() },
        qr/cutoff_epoch parameter required/,
        'Dies when cutoff_epoch is missing'
    );
};

subtest 'prune_single_deleted_user - user not tracked' => sub {
    my ( $du, $mock_map, $mock_logger ) = setup();

    $mock_map->redefine(
        'get_deleted_user' => sub {
            return undef;
        }
    );

    my $result = $du->prune_single_deleted_user('testuser');
    ok $result->{error}, 'Returns error when user not tracked';
    like $result->{error}, qr/not tracked in deleted user metadata/, 'Error message correct';
};

# Note: Testing user existence check requires mocking getpwnam which is difficult
# The actual module logic is correct, but mocking CORE::GLOBAL functions is complex
# These test cases would be better suited for integration tests

subtest 'restore_deleted_user - user not tracked' => sub {
    my ( $du, $mock_map, $mock_logger ) = setup();

    $mock_map->redefine(
        'get_deleted_user' => sub {
            return undef;
        }
    );

    my $result = $du->restore_deleted_user( 'testuser', 'snapshot-123' );
    ok $result->{error}, 'Returns error when user not tracked';
    like $result->{error}, qr/not tracked in deleted user metadata/, 'Error message correct';
};

subtest 'prune_deleted_user_snapshots_before - no deleted users' => sub {
    my ( $du, $mock_map, $mock_logger ) = setup();

    $mock_map->redefine(
        'get_deleted_users' => sub {
            return ();    # No deleted users
        }
    );

    my $cutoff = time() - 90 * 86400;
    my $result = $du->prune_deleted_user_snapshots_before( cutoff_epoch => $cutoff );

    ok !$result->{error}, 'No error when no deleted users exist';
    is $result->{count},                         0,       'Count is 0';
    is ref( $result->{users_affected} ),         'ARRAY', 'users_affected is an array';
    is scalar( @{ $result->{users_affected} } ), 0,       'No users affected';
};

subtest 'prune_deleted_user_snapshots_before - missing parameter' => sub {
    my ( $du, $mock_map, $mock_logger ) = setup();

    like(
        dies { $du->prune_deleted_user_snapshots_before() },
        qr/cutoff_epoch parameter required/,
        'Dies when cutoff_epoch is missing'
    );
};

subtest 'prune_deleted_user_snapshots_before - database not found' => sub {
    my ( $du, $mock_map, $mock_logger ) = setup();

    $mock_map->redefine(
        'get_deleted_users' => sub {
            return (
                { system_user => 'deleteduser1' },
                { system_user => 'deleteduser2' },
            );
        }
    );

    # Mock getpwnam to return undef (users don't exist)
    no warnings 'redefine';
    local *Whostmgr::CometBackup::DeletedUsers::getpwnam = sub { return; };

    # The mock database file doesn't really exist for -f check
    my $cutoff = time() - 90 * 86400;
    my $result = $du->prune_deleted_user_snapshots_before( cutoff_epoch => $cutoff );

    ok $result->{error}, 'Returns error when database not found or has no tables';
    like $result->{error}, qr/Backup run logs database not found|Failed to prune snapshot records/, 'Error message matches expected patterns';
    is $result->{count},                 0,       'Count is 0';
    is ref( $result->{users_affected} ), 'ARRAY', 'users_affected is an array';
};

subtest 'prune_deleted_user_snapshots_before - dry run mode' => sub {
    my ( $du, $mock_map, $mock_logger, $mock_api, $mock_dbi ) = setup();

    $mock_map->redefine(
        'get_deleted_users' => sub {
            return (
                { system_user => 'deleteduser1' },
                { system_user => 'deleteduser2' },
            );
        }
    );

    # Mock getpwnam - let it use CORE::getpwnam which will fail for fake users
    # Or we can redefine it in the module being tested
    no warnings 'redefine';
    local *Whostmgr::CometBackup::DeletedUsers::getpwnam = sub { return; };

    # Create a real temp file so -f check passes
    my $temp_db   = File::Temp->new( UNLINK => 1, SUFFIX => '.db' );
    my $temp_path = $temp_db->filename;

    # Mock the constant to point to temp file
    local $Whostmgr::CometBackup::Constants::RUN_LOG_DB = $temp_path;

    # Mock DBI to return count of records that would be deleted
    my $query_count = 0;
    $mock_dbi->redefine(
        'connect' => sub {
            my $mock_sth = bless { query_count => 0 }, 'MockSTHCount';
            return bless { _mock_sth => $mock_sth }, 'MockDBH';
        }
    );

    my $cutoff = time() - 90 * 86400;
    my $result = $du->prune_deleted_user_snapshots_before(
        cutoff_epoch => $cutoff,
        dry_run      => 1
    );

    ok !$result->{error}, 'No error in dry run mode';
    is ref( $result->{users_affected} ), 'ARRAY', 'users_affected is an array';

    # Count will be 0 because mock returns 0 rows
};

subtest 'prune_deleted_user_snapshots_before - actual deletion' => sub {
    my ( $du, $mock_map, $mock_logger, $mock_api, $mock_dbi ) = setup();

    $mock_map->redefine(
        'get_deleted_users' => sub {
            return (
                { system_user => 'deleteduser1' },
                { system_user => 'deleteduser2' },
            );
        }
    );

    # Mock getpwnam
    no warnings 'redefine';
    local *Whostmgr::CometBackup::DeletedUsers::getpwnam = sub { return; };

    # Create a real temp file so -f check passes
    my $temp_db   = File::Temp->new( UNLINK => 1, SUFFIX => '.db' );
    my $temp_path = $temp_db->filename;

    # Mock the constant to point to temp file
    local $Whostmgr::CometBackup::Constants::RUN_LOG_DB = $temp_path;

    # Mock DBI to simulate successful deletion
    $mock_dbi->redefine(
        'connect' => sub {
            my $mock_sth = bless { rows_deleted => 5 }, 'MockSTHDelete';
            return bless { _mock_sth => $mock_sth }, 'MockDBH';
        }
    );

    my $cutoff = time() - 90 * 86400;
    my $result = $du->prune_deleted_user_snapshots_before( cutoff_epoch => $cutoff );

    ok !$result->{error}, 'No error during deletion';
    is $result->{count},                         10,      'Returns total count of deleted records (5 per user)';
    is ref( $result->{users_affected} ),         'ARRAY', 'users_affected is an array';
    is scalar( @{ $result->{users_affected} } ), 2,       'Two users affected';
};

subtest 'prune_deleted_user_snapshots_before - specific users filter' => sub {
    my ( $du, $mock_map, $mock_logger, $mock_api, $mock_dbi ) = setup();

    $mock_map->redefine(
        'get_deleted_users' => sub {
            return (
                { system_user => 'deleteduser1' },
                { system_user => 'deleteduser2' },
                { system_user => 'deleteduser3' },
            );
        }
    );

    # Mock getpwnam
    no warnings 'redefine';
    local *Whostmgr::CometBackup::DeletedUsers::getpwnam = sub { return; };

    # Create a real temp file so -f check passes
    my $temp_db   = File::Temp->new( UNLINK => 1, SUFFIX => '.db' );
    my $temp_path = $temp_db->filename;

    # Mock the constant to point to temp file
    local $Whostmgr::CometBackup::Constants::RUN_LOG_DB = $temp_path;

    # Mock DBI to simulate successful deletion
    $mock_dbi->redefine(
        'connect' => sub {
            my $mock_sth = bless { rows_deleted => 3 }, 'MockSTHDelete';
            return bless { _mock_sth => $mock_sth }, 'MockDBH';
        }
    );

    my $cutoff = time() - 90 * 86400;
    my $result = $du->prune_deleted_user_snapshots_before(
        cutoff_epoch => $cutoff,
        users        => [ 'deleteduser1', 'deleteduser3' ]    # Filter to specific users
    );

    ok !$result->{error}, 'No error during filtered deletion';
    is $result->{count},                         6,       'Returns count for filtered users (3 per user)';
    is ref( $result->{users_affected} ),         'ARRAY', 'users_affected is an array';
    is scalar( @{ $result->{users_affected} } ), 2,       'Only two filtered users affected';
};

subtest 'prune_deleted_user_snapshots_before - users still exist locally' => sub {
    my ( $du, $mock_map, $mock_logger ) = setup();

    $mock_map->redefine(
        'get_deleted_users' => sub {
            return (
                { system_user => 'deleteduser1' },
                { system_user => 'stillexists' },
            );
        }
    );

    # Mock getpwnam to simulate one user still exists
    no warnings 'redefine';
    local *Whostmgr::CometBackup::DeletedUsers::getpwnam = sub {
        my ($username) = @_;
        return $username eq 'stillexists' ? 1 : undef;
    };

    # The test should filter out 'stillexists'
    my $cutoff = time() - 90 * 86400;
    my $result = $du->prune_deleted_user_snapshots_before( cutoff_epoch => $cutoff );

    # Should fail because database doesn't exist, but proves filtering logic works
    ok $result->{error}, 'Returns error (no database)';

    # The important part is that it filtered out 'stillexists' before trying database
};

subtest 'prune_deleted_user_snapshots_before - database connection error' => sub {
    my ( $du, $mock_map, $mock_logger, $mock_api, $mock_dbi ) = setup();

    $mock_map->redefine(
        'get_deleted_users' => sub {
            return ( { system_user => 'deleteduser1' } );
        }
    );

    # Mock getpwnam
    no warnings 'redefine';
    local *Whostmgr::CometBackup::DeletedUsers::getpwnam = sub { return; };

    # Create a real temp file so -f check passes
    my $temp_db   = File::Temp->new( UNLINK => 1, SUFFIX => '.db' );
    my $temp_path = $temp_db->filename;

    # Mock the constant to point to temp file
    local $Whostmgr::CometBackup::Constants::RUN_LOG_DB = $temp_path;

    # Mock DBI to throw connection error
    $mock_dbi->redefine(
        'connect' => sub {
            die "Connection refused\n";
        }
    );

    my $cutoff = time() - 90 * 86400;
    my $result = $du->prune_deleted_user_snapshots_before( cutoff_epoch => $cutoff );

    ok $result->{error}, 'Returns error on connection failure';
    like $result->{error}, qr/Failed to connect to backup run logs database/, 'Error message correct';
    is $result->{count}, 0, 'Count is 0';
};

done_testing();

sub setup {
    my $mock_map    = Test::MockModule->new('Whostmgr::CometBackup::ProtectedItemMap');
    my $mock_api    = Test::MockModule->new('Whostmgr::CometBackup::CometBackupAPI');
    my $mock_logger = Test::MockModule->new('Whostmgr::CometBackup::Logger');
    my $mock_dbi    = Test::MockModule->new( 'DBI', no_auto => 1 );

    # Mock logger to avoid file system access
    $mock_logger->redefine(
        'new' => sub {
            return bless { log_level => 'info' }, 'Whostmgr::CometBackup::Logger';
        }
    );
    $mock_logger->redefine( 'info'                  => sub { } );
    $mock_logger->redefine( 'warn'                  => sub { } );
    $mock_logger->redefine( 'error'                 => sub { } );
    $mock_logger->redefine( 'debug'                 => sub { } );
    $mock_logger->redefine( '_ensure_log_directory' => sub { return 1; } );
    $mock_logger->redefine( '_rotate_log_if_needed' => sub { return 1; } );
    $mock_logger->redefine( '_write_to_log'         => sub { return 1; } );

    # Default mock for ProtectedItemMap->new
    $mock_map->redefine(
        'new' => sub {
            return bless {}, 'Whostmgr::CometBackup::ProtectedItemMap';
        }
    );

    # Mock DBI connect to return a mock database handle
    $mock_dbi->redefine(
        'connect' => sub {
            return bless {
                _mock_sth => bless {}, 'MockSTH',
              },
              'MockDBH';
        }
    );

    my $du = Whostmgr::CometBackup::DeletedUsers->new();

    return ( $du, $mock_map, $mock_logger, $mock_api, $mock_dbi );
}

# Mock database handle and statement handle
package MockDBH {
    sub prepare    { return $_[0]->{_mock_sth}; }
    sub disconnect { return 1; }
}

package MockSTH {
    sub execute           { return 1; }
    sub fetchrow_hashref  { return undef; }
    sub fetchrow_array    { return (); }
    sub fetchall_arrayref { return []; }
    sub finish            { return 1; }
}

package MockSTHEmpty {
    sub execute           { return 1; }
    sub fetchrow_hashref  { return undef; }
    sub fetchrow_array    { return (); }
    sub fetchall_arrayref { return []; }
    sub finish            { return 1; }
}

package MockSTHCount {
    sub execute { return 1; }

    sub fetchrow_array {
        my ($self) = @_;

        # Return count of 0 for dry run mode
        return (0);
    }
    sub finish { return 1; }
}

package MockSTHDelete {

    sub execute {
        my ($self) = @_;

        # Return number of rows deleted
        return $self->{rows_deleted} || 0;
    }
    sub finish { return 1; }
}
