#!/usr/bin/env perl

# Unit tests for Harness::Catalogue against small FIXTURE catalogue data (not the real 19-part
# catalogue - that gets its own coverage test, test_Harness_Catalogue_real.pl, once it exists).
# This file proves the loader/validator/dependency-resolver mechanism itself is correct.

use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../sneakernet/testing";
use Harness::Catalogue;
use File::Temp qw(tempdir);
use JSON::PP qw(encode_json);

my $passed = 0;
my $failed = 0;

sub ok {
    my ($cond, $desc) = @_;
    if ($cond) { print "  PASS: $desc\n"; $passed++; return 1; }
    else       { print "  FAIL: $desc\n"; $failed++; return 0; }
}

sub dies_ok {
    my ($code, $desc, $like) = @_;
    my $died = 0; my $msg = '';
    eval { $code->(); 1 } or do { $died = 1; $msg = $@; };
    if ($died && defined $like && $msg !~ $like) {
        print "  FAIL: $desc (died, but message didn't match $like: $msg)\n"; $failed++; return;
    }
    ok($died, $desc);
}

sub lives_ok {
    my ($code, $desc) = @_;
    my $ok = eval { $code->(); 1 };
    ok($ok, $desc) or print "    error: $@\n";
}

sub write_json {
    my ($path, $data) = @_;
    open my $fh, '>', $path or die $!;
    print {$fh} encode_json($data);
    close $fh;
}

# ===========================================================================
print "=== basic load: one part, two steps, a dependency ===\n";
# ===========================================================================
{
    my $dir = tempdir(CLEANUP => 1);
    write_json("$dir/reset_profiles.json", { standard => [ { do => 'noop' } ] });
    write_json("$dir/part01.json", {
        part => 1, title => 'Fixture Part', reset => 'standard',
        steps => [
            { id => '1.1', title => 'first', classification => 'assert',
              actions => [ { do => 'write_file', path => '{TMP}/x.txt', content => 'hi' } ] },
            { id => '1.2', title => 'second', depends_on => ['1.1'],
              actions => [ { do => 'read_file', path => '{TMP}/x.txt' } ] },
        ],
    });

    my $cat = Harness::Catalogue->load_dir($dir);
    ok(scalar($cat->parts) == 1, 'one part loaded');
    ok(scalar($cat->all_step_ids) == 2, 'two steps loaded');
    ok($cat->step('1.2')->{depends_on}[0] eq '1.1', 'depends_on preserved');
    ok($cat->step('1.1')->{actions}[0]{path} eq '/storage/testing/tmp/x.txt',
        '{TMP} token expanded inside a nested action hash');
    ok($cat->step('1.1')->{classification} eq 'assert', 'explicit classification preserved');
    ok($cat->step('1.2')->{classification} eq 'assert', 'classification defaults to assert when omitted');
    ok($cat->step('1.1')->{reset} eq 'standard', "step inherits the part's reset profile");
}

# ===========================================================================
print "\n=== unknown keys are fatal, not warnings ===\n";
# ===========================================================================
{
    my $dir = tempdir(CLEANUP => 1);
    write_json("$dir/part01.json", {
        part => 1, title => 'x', typo_field => 'oops',
        steps => [ { id => '1.1', title => 'x' } ],
    });
    dies_ok(sub { Harness::Catalogue->load_dir($dir) },
        'an unknown top-level Part key is a fatal load error', qr/unknown key 'typo_field'/);
}
{
    my $dir = tempdir(CLEANUP => 1);
    write_json("$dir/part01.json", {
        part => 1, title => 'x',
        steps => [ { id => '1.1', title => 'x', expactations => [] } ],
    });
    dies_ok(sub { Harness::Catalogue->load_dir($dir) },
        'an unknown Step key (e.g. a typo of "expectations") is a fatal load error', qr/unknown key/);
}

# ===========================================================================
print "\n=== duplicate step ids across files are fatal ===\n";
# ===========================================================================
{
    my $dir = tempdir(CLEANUP => 1);
    write_json("$dir/part01.json", { part => 1, title => 'x', steps => [ { id => 'dup', title => 'a' } ] });
    write_json("$dir/part02.json", { part => 2, title => 'y', steps => [ { id => 'dup', title => 'b' } ] });
    dies_ok(sub { Harness::Catalogue->load_dir($dir) }, 'a step id reused across two part files is fatal', qr/duplicate step id/);
}

# ===========================================================================
print "\n=== depends_on an unknown step id is fatal at load time, not at run time ===\n";
# ===========================================================================
{
    my $dir = tempdir(CLEANUP => 1);
    write_json("$dir/part01.json", {
        part => 1, title => 'x',
        steps => [ { id => '1.1', title => 'a', depends_on => ['9.9'] } ],
    });
    dies_ok(sub { Harness::Catalogue->load_dir($dir) },
        'a depends_on referencing a step id that does not exist anywhere is caught at load time', qr/unknown step '9\.9'/);
}

# ===========================================================================
print "\n=== invalid classification / config.restore values are fatal ===\n";
# ===========================================================================
{
    my $dir = tempdir(CLEANUP => 1);
    write_json("$dir/part01.json", {
        part => 1, title => 'x',
        steps => [ { id => '1.1', title => 'a', classification => 'sorta-passed' } ],
    });
    dies_ok(sub { Harness::Catalogue->load_dir($dir) }, 'an invalid classification value is refused', qr/unknown classification/);
}
{
    my $dir = tempdir(CLEANUP => 1);
    write_json("$dir/part01.json", {
        part => 1, title => 'x',
        steps => [ { id => '1.1', title => 'a', config => { restore => 'sometimes' } } ],
    });
    dies_ok(sub { Harness::Catalogue->load_dir($dir) }, 'an invalid config.restore value is refused', qr/unknown config\.restore/);
}

# ===========================================================================
print "\n=== resolve_selection() pulls in transitive dependencies ===\n";
# ===========================================================================
{
    my $dir = tempdir(CLEANUP => 1);
    write_json("$dir/part01.json", {
        part => 1, title => 'x',
        steps => [
            { id => 'a', title => 'a' },
            { id => 'b', title => 'b', depends_on => ['a'] },
            { id => 'c', title => 'c', depends_on => ['b'] },
            { id => 'd', title => 'd' },
        ],
    });
    my $cat = Harness::Catalogue->load_dir($dir);

    my @sel = $cat->resolve_selection(['c'], 0);
    ok(scalar(@sel) == 3 && "@sel" eq 'a b c', "selecting 'c' (needs b needs a) pulls in the whole chain: @sel")
        or print "    got: @sel\n";

    my @selForced = $cat->resolve_selection(['c'], 1);
    ok(scalar(@selForced) == 1 && $selForced[0] eq 'c',
        "--force-partial selecting 'c' alone returns exactly ['c'], dependencies NOT auto-included");

    my @selD = $cat->resolve_selection(['d'], 0);
    ok(scalar(@selD) == 1 && $selD[0] eq 'd', "selecting 'd' (no dependencies) returns just ['d']");

    my @selBoth = $cat->resolve_selection(['d', 'c'], 0);
    ok("@selBoth" eq 'a b c d', "selecting multiple ids returns them in CATALOGUE order, not request order: @selBoth");
}

# ===========================================================================
print "\n=== reset profiles load and are retrievable ===\n";
# ===========================================================================
{
    my $dir = tempdir(CLEANUP => 1);
    write_json("$dir/reset_profiles.json", {
        standard => [ { do => 'empty_dir', path => '{TRANSPORT}' } ],
        none     => [],
    });
    write_json("$dir/part01.json", { part => 1, title => 'x', steps => [ { id => '1.1', title => 'a' } ] });
    my $cat = Harness::Catalogue->load_dir($dir);
    my $profile = $cat->reset_profile('standard');
    ok(ref($profile) eq 'ARRAY', 'standard reset profile is an array');
    ok($profile->[0]{path} eq '/storage/testing/transport', 'reset profile actions also get token-expanded');
}

# ===========================================================================
print "\n=== steps_in_part() ===\n";
# ===========================================================================
{
    my $dir = tempdir(CLEANUP => 1);
    write_json("$dir/part01.json", { part => 1, title => 'x', steps => [ { id => '1.1', title => 'a' }, { id => '1.2', title => 'b' } ] });
    write_json("$dir/part02.json", { part => 2, title => 'y', steps => [ { id => '2.1', title => 'c' } ] });
    my $cat = Harness::Catalogue->load_dir($dir);
    my @p1 = $cat->steps_in_part(1);
    ok(scalar(@p1) == 2, 'steps_in_part(1) returns exactly the two Part-1 steps');
    my @p2 = $cat->steps_in_part(2);
    ok(scalar(@p2) == 1, 'steps_in_part(2) returns exactly the one Part-2 step');
}

# ===========================================================================
print "\n=== malformed JSON is a clear load-time error ===\n";
# ===========================================================================
{
    my $dir = tempdir(CLEANUP => 1);
    open my $fh, '>', "$dir/part01.json" or die $!;
    print {$fh} "{ not valid json ";
    close $fh;
    dies_ok(sub { Harness::Catalogue->load_dir($dir) }, 'malformed JSON is refused with a clear message', qr/not valid JSON/);
}

print "\n=== Summary ===\n";
print "Passed: $passed\n";
print "Failed: $failed\n";
exit($failed > 0 ? 1 : 0);
