#!/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;

BEGIN {
    $INC{'Cpanel/Config/LoadCpUserFile.pm'} = 1;
    $INC{'Cpanel/DBI/SQLite.pm'}            = 1;
    $INC{'Cpanel/Exception.pm'}             = 1;
    $INC{'Cpanel/JSON.pm'}                  = 1;
    $INC{'Cpanel/Mkdir.pm'}                 = 1;
    $INC{'Cpanel/PwCache.pm'}               = 1;
    $INC{'Data/UUID.pm'}                    = 1;
    $INC{'DBI.pm'}                          = 1;
    $INC{'Time/HiRes.pm'}                   = 1;

    package Cpanel::Config::LoadCpUserFile;
    sub new { return bless {}, shift; }

    package Cpanel::DBI::SQLite;
    sub connect    { return bless {}, shift; }
    sub prepare    { return bless {}, 'MockSTH'; }
    sub disconnect { }

    package Cpanel::Exception;
    sub new { return bless {}, shift; }

    package Cpanel::JSON;
    sub Dump { return shift; }
    sub Load { return shift; }

    package Cpanel::Mkdir;
    sub ensure_directory_existence_and_mode { return 1; }

    package Cpanel::PwCache;
    sub new { return bless {}, shift; }

    package Data::UUID;
    our $UUID_COUNTER = 0;
    sub new { return bless {}, shift; }

    sub create_str {
        $UUID_COUNTER++;
        return sprintf( '00000000-0000-0000-0000-%012d', $UUID_COUNTER );
    }

    package DBI;
    sub new { return bless {}, shift; }

    package Time::HiRes;

    # Don't override anything, just make sure the module loads

    package MockSTH;
    sub execute           { return 1; }
    sub fetchrow_hashref  { return undef; }
    sub fetchall_arrayref { return []; }
}

use Whostmgr::CometBackup::ProtectedItemMap ();

my @mock_files = (
    Test::MockFile->file('/var/cpanel/comet/protected_items.db'),
);

subtest 'ProtectedItemMap - new instance' => sub {
    my $map = Whostmgr::CometBackup::ProtectedItemMap->new();

    ok( defined $map, 'ProtectedItemMap instance created' );
    is( ref $map,              'Whostmgr::CometBackup::ProtectedItemMap', 'Correct class' );
    is( $map->{database_file}, '/var/cpanel/comet/protected_items.db',    'Database file path set' );
};

subtest 'ProtectedItemMap - create_protected_items_for_user_job missing job_uuid' => sub {
    my $map = Whostmgr::CometBackup::ProtectedItemMap->new();

    like(
        dies {
            $map->create_protected_items_for_user_job(
                system_user => 'testuser',
            );
        },
        qr/job_uuid parameter required/,
        'Dies when job_uuid is missing'
    );
};

subtest 'ProtectedItemMap - create_protected_items_for_user_job missing system_user' => sub {
    my $map = Whostmgr::CometBackup::ProtectedItemMap->new();

    like(
        dies {
            $map->create_protected_items_for_user_job(
                job_uuid => '00000000-0000-0000-0000-000000000001',
            );
        },
        qr/system_user parameter required/,
        'Dies when system_user is missing'
    );
};

subtest 'ProtectedItemMap - create_protected_items_for_user_job with item_types' => sub {
    my $map = Whostmgr::CometBackup::ProtectedItemMap->new();

    eval {
        my @item_uids = $map->create_protected_items_for_user_job(
            job_uuid    => '00000000-0000-0000-0000-000000000001',
            system_user => 'testuser',
            item_types  => [ 'homedir_files_folders', 'databases' ],
        );
    };
    ok( 1, 'create_protected_items_for_user_job handles valid parameters' );
};

subtest 'ProtectedItemMap - get_protected_items_for_user' => sub {
    my $map = Whostmgr::CometBackup::ProtectedItemMap->new();

    eval {
        my @items = $map->get_protected_items_for_user(
            system_user => 'testuser',
            job_uuid    => '00000000-0000-0000-0000-000000000001',
        );
        ok( ref \@items eq 'ARRAY', 'Returns array of protected items' );
    };
    ok( 1, 'get_protected_items_for_user handles valid parameters' );
};

subtest 'ProtectedItemMap - get_protected_items_for_job' => sub {
    my $map = Whostmgr::CometBackup::ProtectedItemMap->new();

    eval {
        my @items = $map->get_protected_items_for_job(
            job_uuid => '00000000-0000-0000-0000-000000000001',
        );
        ok( ref \@items eq 'ARRAY', 'Returns array of protected items' );
    };
    ok( 1, 'get_protected_items_for_job handles valid parameters' );
};

subtest 'ProtectedItemMap - _glob_to_regex' => sub {
    my $map = Whostmgr::CometBackup::ProtectedItemMap->new();

    # Test empty/undefined patterns
    is( $map->_glob_to_regex(undef), '', 'undef returns empty string' );
    is( $map->_glob_to_regex(''),    '', 'empty string returns empty string' );

    # Test patterns that look like regex (should pass through unchanged)
    is( $map->_glob_to_regex('^start'),    '^start',    'regex starting with ^ passes through' );
    is( $map->_glob_to_regex('end$'),      'end$',      'regex ending with $ passes through' );
    is( $map->_glob_to_regex('(?:group)'), '(?:group)', 'regex with non-capturing group passes through' );
    is( $map->_glob_to_regex('test\\d+'),  'test\\d+',  'regex with \\d passes through' );

    # Test exact conversions for extension patterns (*.ext)
    is( $map->_glob_to_regex('*.txt'),    '.*\.txt$',     '*.txt converts to .*\.txt$' );
    is( $map->_glob_to_regex('*.test'),   '.*\.test$',    '*.test converts to .*\.test$' );
    is( $map->_glob_to_regex('*.log'),    '.*\.log$',     '*.log converts to .*\.log$' );
    is( $map->_glob_to_regex('*.tar.gz'), '.*\.tar\.gz$', '*.tar.gz converts to .*\.tar\.gz$' );

    # Test exact conversions for directory wildcard patterns (dir/*)
    is( $map->_glob_to_regex('cache/*'),   'cache/.*',    'cache/* converts to cache/.*' );
    is( $map->_glob_to_regex('tmp/*'),     'tmp/.*',      'tmp/* converts to tmp/.*' );
    is( $map->_glob_to_regex('var/log/*'), 'var\/log/.*', 'var/log/* converts to var\/log/.*' );

    # Test exact conversions for recursive patterns (**/name)
    # ** matches zero or more path components, so **/cache matches cache at root OR nested
    is( $map->_glob_to_regex('**/cache'),        '(?:^|.*/)cache(?:/.*)?$',        '**/cache converts to (?:^|.*/)cache(?:/.*)?$' );
    is( $map->_glob_to_regex('**/node_modules'), '(?:^|.*/)node_modules(?:/.*)?$', '**/node_modules converts to (?:^|.*/)node_modules(?:/.*)?$' );

    # Test recursive patterns with trailing wildcard (**/dir/*)
    is( $map->_glob_to_regex('**/cache/*'), '(?:^|.*/)cache/.*', '**/cache/* converts to (?:^|.*/)cache/.*' );
    is( $map->_glob_to_regex('**/tmp/*'),   '(?:^|.*/)tmp/.*',   '**/tmp/* converts to (?:^|.*/)tmp/.*' );

    # Test complex recursive patterns (dir/**/*.ext) - ** matches zero or more components
    is( $map->_glob_to_regex('public_html/**/*.obj'), 'public_html\/.*[^/]*\.obj$', 'public_html/**/*.obj handles recursive glob with extension' );
    is( $map->_glob_to_regex('src/**/*.js'),          'src\/.*[^/]*\.js$',          'src/**/*.js handles recursive glob with extension' );

    # Test nested ** patterns (dir/**/subdir/*.ext) - ** in the middle matches zero or more components
    is( $map->_glob_to_regex('mail/**/new/*.obj'), 'mail\/.*new\/[^/]*\.obj$', 'mail/**/new/*.obj handles nested ** with zero or more components' );
    is( $map->_glob_to_regex('var/**/log/*.txt'),  'var\/.*log\/[^/]*\.txt$',  'var/**/log/*.txt handles nested ** with zero or more components' );

    # Test exact conversions for plain names (match anywhere as path segment)
    is( $map->_glob_to_regex('exclude-this'), '(^|.*/)exclude\-this(/.*)?$', 'exclude-this gets proper anchoring' );
    is( $map->_glob_to_regex('node_modules'), '(^|.*/)node_modules(/.*)?$',  'node_modules gets proper anchoring' );
    is( $map->_glob_to_regex('.git'),         '(^|.*/)\\.git(/.*)?$',        '.git gets proper anchoring' );

    # Test exact conversions for absolute paths
    is( $map->_glob_to_regex('/var/cache'), '\\/var\\/cache$', '/var/cache converts correctly' );
    is( $map->_glob_to_regex('/tmp'),       '\\/tmp$',         '/tmp converts correctly' );

    # Test ? wildcard conversion
    is( $map->_glob_to_regex('file?.txt'), 'file.\\.txt$', 'file?.txt converts ? to . for single char' );

    # Test metacharacter escaping in plain names
    is( $map->_glob_to_regex('file.txt'),  '(^|.*/)file\\.txt(/.*)?$',  'dot is escaped in file.txt' );
    is( $map->_glob_to_regex('file+name'), '(^|.*/)file\\+name(/.*)?$', 'plus is escaped in file+name' );

    # Verify the regex patterns are valid (can be compiled) and match expected strings
    # Test against full paths (including homedir) as Comet would see them
    my %test_cases = (
        '*.txt' => {
            regex            => '.*\.txt$',
            should_match     => [ '/home/user/file.txt', '/home/user/sub/dir/file.txt', '/var/log/app.txt', 'file.txt' ],
            should_not_match => [ '/home/user/file.log', '/home/user/file.txt.bak',     '/home/user/txtfile' ],
        },
        '*.test' => {
            regex            => '.*\.test$',
            should_match     => [ '/home/user/something.test', '/home/user/foo.bar.test', 'unit.test' ],
            should_not_match => [ '/home/user/test.something', '/home/user/test',         '/home/user/testing' ],
        },
        '*.this' => {
            regex            => '.*\.this$',
            should_match     => [ '/home/alice/file.this', '/tmp/a.this' ],
            should_not_match => [ '/home/alice/this.file', '/home/alice/this', '/home/alice/thisfile' ],
        },
        'cache/*' => {
            regex            => 'cache/.*',
            should_match     => [ '/home/user/cache/file.txt', '/home/user/cache/sub/deep/file', '/home/user/cache/' ],
            should_not_match => [ '/home/user/other/file.txt', '/home/user/cache' ],
        },
        '**/cache' => {
            regex            => '(?:^|.*/)cache(?:/.*)?$',
            should_match     => [ '/home/user/app/cache', '/home/user/cache',     'cache', '/a/b/c/cache', '/home/user/cache/subdir' ],
            should_not_match => [ '/home/user/mycache',   '/home/user/cache_old', '/home/user/old_cache' ],
        },
        '**/cache/*' => {
            regex            => '(?:^|.*/)cache/.*',
            should_match     => [ '/home/user/app/cache/data.tmp', '/home/user/cache/file', 'cache/foo', '/a/b/cache/x/y/z' ],
            should_not_match => [ '/home/user/mycache/file', '/home/user/cache', '/home/user/cache_dir/file' ],
        },
        'node_modules' => {
            regex            => '(^|.*/)node_modules(/.*)?$',
            should_match     => [ '/home/user/app/node_modules',    '/home/user/node_modules',     'node_modules', '/home/user/node_modules/lodash' ],
            should_not_match => [ '/home/user/node_modules_backup', '/home/user/old_node_modules', '/home/user/node_modulesxyz' ],
        },
        'public_html/**/*.obj' => {
            regex            => 'public_html\/.*[^/]*\.obj$',
            should_match     => [ '/home/user/public_html/file.obj', '/home/user/public_html/assets/model.obj', '/home/user/public_html/a/b/c/d.obj' ],
            should_not_match => [ '/home/user/other_dir/file.obj',   '/home/user/public_html/file.txt',         '/home/user/public_htmlx/file.obj' ],
        },
        'mail/**/new/*.obj' => {
            regex            => 'mail\/.*new\/[^/]*\.obj$',
            should_match     => [ '/home/user/mail/new/file.obj', '/home/user/mail/foo/new/a.obj',    '/home/user/mail/a/b/c/new/x.obj' ],
            should_not_match => [ '/home/user/mail/old/file.obj', '/home/user/mail/new/sub/file.obj', '/home/user/mail/newish/file.obj' ],
        },

        # Edge cases for ** matching zero or more path components
        'app/**/config.json' => {
            regex            => 'app\/.*config\.json$',
            should_match     => [ '/home/user/app/config.json', '/home/user/app/sub/config.json', '/home/user/app/a/b/c/config.json' ],
            should_not_match => [ '/home/user/app/config.yaml', '/home/user/app/config.json.bak' ],
        },
        'logs/**/*.log' => {
            regex            => 'logs\/.*[^/]*\.log$',
            should_match     => [ '/home/user/logs/app.log', '/home/user/logs/2024/01/app.log', '/home/user/logs/error.log' ],
            should_not_match => [ '/home/user/logs/app.txt', '/home/user/logs/logfile',         '/home/user/logs/app.log.old' ],
        },
        'src/**/test/*' => {
            regex            => 'src\/.*test\/[^/]*$',
            should_match     => [ '/home/user/src/test/file.js',    '/home/user/src/foo/test/bar.ts', '/home/user/src/a/b/test/c' ],
            should_not_match => [ '/home/user/src/testing/file.js', '/home/user/src/test/sub/file.js' ],
        },
        'a/**/b/**/c' => {
            regex            => 'a\/.*b\/.*c$',
            should_match     => [ '/home/user/a/b/c', '/home/user/a/x/b/y/c', '/home/user/a/1/2/b/3/4/c' ],
            should_not_match => [ '/home/user/a/x/y', '/home/user/a/c',       '/home/user/b/a/c' ],
        },

        # ** with deeper nesting
        'proj/**/src/**/test/*.spec.js' => {
            regex            => 'proj\/.*src\/.*test\/[^/]*\.spec\.js$',
            should_match     => [ '/home/user/proj/src/test/foo.spec.js', '/home/user/proj/app/src/unit/test/bar.spec.js', '/home/user/proj/src/test/a.spec.js' ],
            should_not_match => [ '/home/user/proj/src/test/foo.js',      '/home/user/proj/src/test/sub/foo.spec.js',      '/home/user/proj/src/testing/foo.spec.js' ],
        },

        # Additional edge cases
        '**/*.tmp' => {
            regex            => '.*[^/]*\.tmp$',
            should_match     => [ '/home/user/file.tmp',  '/home/user/sub/file.tmp', 'file.tmp', '/a/b/c/d.tmp' ],
            should_not_match => [ '/home/user/file.temp', '/home/user/tmpfile', '/home/user/file.tmp.bak' ],
        },
        'backup-*' => {
            regex            => 'backup\-[^/]*$',
            should_match     => [ '/home/user/backup-2024', '/home/user/sub/backup-old', 'backup-test' ],
            should_not_match => [ '/home/user/backup', '/home/user/backup-2024/file' ],
        },
        '*.tar.gz' => {
            regex            => '.*\.tar\.gz$',
            should_match     => [ '/home/user/archive.tar.gz', '/tmp/backup.tar.gz',    'file.tar.gz' ],
            should_not_match => [ '/home/user/archive.tar',    '/home/user/archive.gz', '/home/user/tar.gz' ],
        },
        '.git' => {
            regex            => '(^|.*/)\\.git(/.*)?$',
            should_match     => [ '/home/user/project/.git',    '/home/user/.git', '.git', '/home/user/project/.git/config' ],
            should_not_match => [ '/home/user/project/.github', '/home/user/project/.gitignore', '/home/user/project/git' ],
        },
        'tmp' => {
            regex            => '(^|.*/)tmp(/.*)?$',
            should_match     => [ '/home/user/tmp',  '/home/user/app/tmp', 'tmp', '/home/user/tmp/file.txt' ],
            should_not_match => [ '/home/user/temp', '/home/user/mytmp',   '/home/user/tmp2' ],
        },
    );

    for my $pattern ( sort keys %test_cases ) {
        my $expected = $test_cases{$pattern}{regex};
        my $actual   = $map->_glob_to_regex($pattern);
        is( $actual, $expected, "glob '$pattern' produces exact regex '$expected'" );

        # Verify regex compiles
        my $compiled;
        eval { $compiled = qr/$actual/; };
        ok( !$@, "regex for '$pattern' compiles successfully" );

        # Verify match behavior - now handles arrayrefs
        my $should_match = $test_cases{$pattern}{should_match};
        if ($should_match) {
            my @matches = ref $should_match eq 'ARRAY' ? @$should_match : ($should_match);
            for my $path (@matches) {
                like( $path, $compiled, "'$pattern' regex matches: $path" );
            }
        }

        my $should_not_match = $test_cases{$pattern}{should_not_match};
        if ($should_not_match) {
            my @non_matches = ref $should_not_match eq 'ARRAY' ? @$should_not_match : ($should_not_match);
            for my $path (@non_matches) {
                unlike( $path, $compiled, "'$pattern' regex does NOT match: $path" );
            }
        }
    }
};

done_testing();
