#!/usr/bin/env perl

# test_resolveSnapshotRoot.pl
#
# Tests validateBackup's snapshot-root resolution: resolveSnapshotRoot, findLegacyMountPoint.
#
# validateBackup is a script, not a requireable module (unguarded top-level logic runs on load),
# so the functions under test are copied here verbatim, per this project's "extract a sub into a
# stub harness" testing convention (see test_destroyedSourceDataset.pl). findLegacyMountPoint
# calls ZFS_Utils::runCmd/sshCommand for real, so a fake 'mount' is put on PATH the same way
# test_destroyedSourceDataset.pl fakes 'zfs' - and, for the remote-host cases, a fake 'ssh' that
# just runs the wrapped command locally. THIS IS THE FIRST 'ssh' SHIM IN testLibrary/: earlier
# tests only ever exercised the local-host path, where sshCommand() returns its argument
# unchanged and no ssh binary is ever invoked. Future tests needing a remote-host code path can
# copy the shim below.
#
# IF YOU CHANGE validateBackup's copies of these functions, change them here too.
#
# Author: R. W. Rodolico <rodo@dailydata.net>
# Created: September 2026

use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/..";
use ZFS_Utils qw(runCmd shellQuote sshCommand);
use File::Temp qw(tempdir);

$ZFS_Utils::displayLogsOnConsole = 0;
$ZFS_Utils::logFileName = '/tmp/test_resolveSnapshotRoot.log';
unlink $ZFS_Utils::logFileName if -f $ZFS_Utils::logFileName;

my ( $passed, $failed ) = ( 0, 0 );

sub ok {
   my ( $condition, $description ) = @_;
   if ($condition) { print "  PASS: $description\n"; $passed++; }
   else            { print "  FAIL: $description\n"; $failed++; }
}

# ---------------------------------------------------------------------------------------------
# Functions under test, copied verbatim from validateBackup
# ---------------------------------------------------------------------------------------------

sub findLegacyMountPoint {
   my ( $dataset, $host ) = @_;
   my @lines = runCmd( sshCommand( $host, 'mount -t zfs' ) );
   foreach my $line (@lines) {
      # FreeBSD 'mount' format: "pool/dataset on /path (zfs, local, noatime)"
      return $1 if $line =~ m{^\Q$dataset\E\s+on\s+(\S+)\s};
   }
   return '';
}

sub resolveSnapshotRoot {
   my ( $dataset, $dsMeta, $snapName, $host ) = @_;

   return ( '', "dataset '$dataset' is not present on this side", 0 ) unless $dsMeta;

   my $type = $dsMeta->{type} // '';
   return ( '', "dataset '$dataset' is a zvol (type=volume) and contains no files", 1 )
      if $type eq 'volume';

   my $mountPoint = $dsMeta->{mountpoint} // '';
   if ( $mountPoint eq 'none' ) {
      return ( '', "dataset '$dataset' has mountpoint=none, so .zfs is unreachable"
         . " (normal for a container dataset; its children are handled separately)", 1 );
   }
   elsif ( $mountPoint eq 'legacy' ) {
      $mountPoint = findLegacyMountPoint( $dataset, $host );
      return ( '', "dataset '$dataset' has mountpoint=legacy and is not currently mounted", 0 )
         if $mountPoint eq '';
   }
   elsif ( ( $dsMeta->{mounted} // '' ) ne 'yes' ) {
      return ( '', "dataset '$dataset' is not mounted (mounted=no), so .zfs is unreachable", 0 );
   }

   return ( "$mountPoint/.zfs/snapshot/$snapName", '', 0 );
}

# ---------------------------------------------------------------------------------------------
# Fake 'mount' and 'ssh' on PATH
# ---------------------------------------------------------------------------------------------

my $bindir = tempdir( CLEANUP => 1 );

open( my $mountFh, '>', "$bindir/mount" ) or die $!;
print { $mountFh } <<'EOF';
#!/bin/sh
# Fake 'mount -t zfs' output: two legacy-mounted datasets, matching FreeBSD's format.
echo "tank/legacyds on /mnt/legacy (zfs, local, noatime)"
echo "tank/legacyds2 on /mnt/legacy2 (zfs, local)"
EOF
close($mountFh);
chmod 0755, "$bindir/mount";

open( my $sshFh, '>', "$bindir/ssh" ) or die $!;
print { $sshFh } <<'EOF';
#!/bin/sh
# Minimal ssh stand-in: strip '-o key=value' options and the hostname, then run the remaining
# argument as a shell command LOCALLY. Good enough to exercise sshCommand()'s remote-host code
# path without a real network target - the same throwaway pattern used ad hoc while testing
# Stages 3-6, made permanent here since this is the first test that actually needs it.
while [ "$1" = "-o" ]; do shift 2; done
shift
exec /bin/sh -c "$*"
EOF
close($sshFh);
chmod 0755, "$bindir/ssh";

local $ENV{PATH} = "$bindir:$ENV{PATH}";

# ---------------------------------------------------------------------------------------------
print "\n=== resolveSnapshotRoot: dataset states ===\n";
# ---------------------------------------------------------------------------------------------
{
   my ( $root, $reason, $expected ) = resolveSnapshotRoot( 'tank/gone', undef, 'daily-10', '' );
   ok( $root eq '' && $reason =~ /not present on this side/,
      "a dataset absent from getDatasetProperties output (undef \$dsMeta) is reported, not a crash" );
   ok( $expected == 0, "an absent dataset is NOT an 'expected' skip - it's a real resolution gap" );
}
{
   my ( $root, $reason, $expected ) =
      resolveSnapshotRoot( 'tank/vol1', { type => 'volume' }, 'daily-10', '' );
   ok( $root eq '' && $reason =~ /zvol/, "a zvol (type=volume) is reported and never given a snapshot root" );
   ok( $expected == 1, "a zvol IS an 'expected' skip - a permanent property of the dataset" );
}
{
   my ( $root, $reason, $expected ) = resolveSnapshotRoot(
      'tank/container', { type => 'filesystem', mountpoint => 'none' }, 'daily-10', '' );
   ok( $root eq '' && $reason =~ /mountpoint=none/, "mountpoint=none is reported as unreachable" );
   ok( $expected == 1, "mountpoint=none IS an 'expected' skip - normal for a container dataset" );
}
{
   my ( $root, $reason, $expected ) = resolveSnapshotRoot(
      'tank/noauto', { type => 'filesystem', mountpoint => '/tank/noauto', mounted => 'no' },
      'daily-10', '' );
   ok( $root eq '' && $reason =~ /mounted=no/, "mounted=no (e.g. canmount=noauto) is reported as unreachable" );
   ok( $expected == 0, "mounted=no is NOT an 'expected' skip - it needs an operator's attention" );
}
{
   my ( $root, $reason, $expected ) = resolveSnapshotRoot(
      'tank/data', { type => 'filesystem', mountpoint => '/tank/data', mounted => 'yes' },
      'daily-10', '' );
   ok( $root eq '/tank/data/.zfs/snapshot/daily-10' && $reason eq '' && $expected == 0,
      "the normal case resolves to <mountpoint>/.zfs/snapshot/<snap> with no skip" );
}

# ---------------------------------------------------------------------------------------------
print "\n=== resolveSnapshotRoot + findLegacyMountPoint: local host ===\n";
# ---------------------------------------------------------------------------------------------
{
   my ( $root, $reason, $expected ) = resolveSnapshotRoot(
      'tank/legacyds', { type => 'filesystem', mountpoint => 'legacy' }, 'daily-10', '' );
   ok( $root eq '/mnt/legacy/.zfs/snapshot/daily-10',
      "mountpoint=legacy is resolved via the mount table (local host)" );
   ok( $expected == 0, "a resolvable legacy mount is not a skip at all" );
}
{
   my ( $root, $reason, $expected ) = resolveSnapshotRoot(
      'tank/legacyds-unmounted', { type => 'filesystem', mountpoint => 'legacy' }, 'daily-10', '' );
   ok( $root eq '' && $reason =~ /not currently mounted/,
      "a legacy dataset absent from the mount table is reported as not mounted" );
   ok( $expected == 0, "an unmounted legacy dataset is NOT an 'expected' skip" );
}
{
   # The regex is anchored with \Q...\E and requires whitespace immediately after the dataset
   # name, so 'tank/legacyds' must not match the mount-table line for 'tank/legacyds2'.
   my $mountPoint = findLegacyMountPoint( 'tank/legacyds', '' );
   ok( $mountPoint eq '/mnt/legacy',
      "a dataset name that is a PREFIX of another mounted dataset's name does not cross-match it" );
}

# ---------------------------------------------------------------------------------------------
print "\n=== resolveSnapshotRoot + findLegacyMountPoint: remote host (first ssh shim) ===\n";
# ---------------------------------------------------------------------------------------------
{
   my ( $root, $reason, $expected ) = resolveSnapshotRoot(
      'tank/legacyds2', { type => 'filesystem', mountpoint => 'legacy' }, 'weekly-01', 'activehost' );
   ok( $root eq '/mnt/legacy2/.zfs/snapshot/weekly-01',
      "the same legacy-mount resolution works over the fake ssh transport, for a 'remote' host" );
}
{
   my ( $root, $reason ) = resolveSnapshotRoot(
      'tank/never-mounted-remote', { type => 'filesystem', mountpoint => 'legacy' }, 'daily-10', 'activehost' );
   ok( $root eq '' && $reason =~ /not currently mounted/,
      "a legacy dataset not in the REMOTE mount table is reported the same way as locally" );
}

print "\nTest Summary: $passed passed, $failed failed\n";
exit( $failed == 0 ? 0 : 1 );
