#!/usr/bin/perl
# Coding assistance from Claude Code
#
# Tests for usr/sbin/rebuild-iptables.
#
# Each test case is a directory under t/data/rebuild-iptables/ containing:
#   fragments/       -- a directory holding the input fragment file(s)
#                       passed via --fragment-dir
#
# Success cases additionally contain:
#   expected.v4      -- the expected --print4 output for the case
#   expected.v6      -- the expected --print6 output for the case
#
# Expected-failure cases contain instead:
#   expected.err     -- a regex (one line) that the script's stderr
#                       must match; the script must also exit non-zero
#
# For each case, the test invokes rebuild-iptables --fragment-dir <dir>
# --no-validate --print4 (and --print6) and asserts that the captured
# output is byte-identical to the expected file.  --no-validate is used
# so the tests do not require iptables-restore to be installed or
# runnable as root.

use 5.014;
use strict;
use warnings;

use English qw(-no_match_vars);
use File::Spec;
use FindBin qw($Bin);
use IPC::Run qw(run);
use Test::More;

# Locate the script and the fixture root, both relative to this file so
# the test can be invoked from any working directory.
my $script   = File::Spec->catfile($Bin, qw(.. .. usr sbin rebuild-iptables));
my $data_dir = File::Spec->catdir($Bin,  qw(.. data rebuild-iptables));

if (!-x $script) {
    plan(skip_all => "script not executable: $script");
}
if (!-d $data_dir) {
    plan(skip_all => "fixture directory not found: $data_dir");
}

# Discover test cases by listing subdirectories of the fixture root.
opendir(my $dh, $data_dir)
  or BAIL_OUT("cannot open $data_dir: $ERRNO");
my @cases = sort grep {
    !m{ \A [.] }xms && -d File::Spec->catdir($data_dir, $_)
} readdir($dh);
closedir($dh);

if (!@cases) {
    plan(skip_all => "no test cases found under $data_dir");
}

# Count tests: success cases contribute 2, failure cases contribute 2
# (exit-code check + stderr-match check).
my $test_count = 0;
for my $case (@cases) {
    my $case_dir = File::Spec->catdir($data_dir, $case);
    if (-f File::Spec->catfile($case_dir, 'expected.err')) {
        $test_count += 2;
    } else {
        $test_count += 2;
    }
}
plan(tests => $test_count);

# Run the script with the given arguments and return its captured
# stdout, stderr, and exit code.  Returns (undef, undef, undef) if the
# script could not be launched at all.
sub run_script {
    my (@args) = @_;
    my ($out, $err) = (q{}, q{});
    my $ok = eval {
        run([$script, @args], \undef, \$out, \$err);
        1;
    };
    if (!$ok) {
        diag("could not run script: $EVAL_ERROR");
        return (undef, undef, undef);
    }
    my $rc = $CHILD_ERROR >> 8;
    return ($out, $err, $rc);
}

# Read the entire contents of a file as a single string.  Returns undef
# on failure.
sub slurp {
    my ($path) = @_;
    open(my $fh, '<', $path) or do {
        diag("cannot open $path: $ERRNO");
        return;
    };
    local $INPUT_RECORD_SEPARATOR = undef;
    my $contents = <$fh>;
    close($fh);
    return $contents;
}

for my $case (@cases) {
    my $case_dir     = File::Spec->catdir($data_dir, $case);
    my $fragment_dir = File::Spec->catdir($case_dir, 'fragments');
    my $err_path     = File::Spec->catfile($case_dir, 'expected.err');

    if (-f $err_path) {
        # Expected-failure case: the script must exit non-zero and its
        # stderr must match the regex in expected.err.
        my $pattern = slurp($err_path);
        chomp $pattern;
        my (undef, $err, $rc) = run_script(
            '--fragment-dir', $fragment_dir,
            '--no-validate',
            '--print4',
        );
        isnt($rc, 0, "$case exits non-zero");
        like($err, qr/\Q$pattern\E/, "$case stderr matches /$pattern/");
    } else {
        # Success case: --print4 and --print6 outputs must each match
        # their respective expected file byte-for-byte.
        for my $proto (qw(v4 v6)) {
            my $flag          = ($proto eq 'v4') ? '--print4' : '--print6';
            my $expected_path = File::Spec->catfile($case_dir, "expected.$proto");
            my $expected      = slurp($expected_path);
            my ($actual, $err, $rc) = run_script(
                '--fragment-dir', $fragment_dir,
                '--no-validate',
                $flag,
            );
            if ($rc != 0) {
                diag("script exited rc=$rc; stderr was: $err");
            }
            is($actual, $expected, "$case $flag matches expected.$proto");
        }
    }
}
