#! /usr/bin/env perl

# Simplified BSD License (FreeBSD License)
#
# Copyright (c) 2026, Daily Data Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
#    list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

# Harness::Report - renders TESTING.log. See TESTING_automation.md section 12.
#
# TESTING.log is the test-RESULTS report, the automated equivalent of the PASS/FAIL sheet a
# human fills in while working through TESTING.md by hand - despite the '.log' extension it
# is not a debug/trace log (that's the audit JSONL, kept separately).

package Harness::Report;

use strict;
use warnings;
use POSIX qw(strftime);

# render(%opts) -> report text
#   run_id, start_time, end_time (epoch seconds), host, svn_rev, mode,
#   sneakernet_version, zfsutils_version, harness_version,
#   baseline_maxdelta, include_optional,
#   results     - arrayref of Harness::Runner result hashes
#   part_titles - hashref { partNum => title }
#   followups   - arrayref of strings (deliberately-deferred items)
sub render {
    my (%o) = @_;
    my @lines;

    push @lines, '=' x 78;
    push @lines, 'sneakernet TESTING.md automation - TESTING.log';
    push @lines, '=' x 78;
    push @lines, '';
    push @lines, "Run id:              $o{run_id}";
    push @lines, 'Started:             ' . _fmt($o{start_time});
    push @lines, 'Ended:               ' . _fmt($o{end_time});
    push @lines, "Duration:            " . _duration($o{end_time} - $o{start_time}) if defined $o{start_time} && defined $o{end_time};
    push @lines, "Host:                $o{host}";
    push @lines, "SVN revision tested: $o{svn_rev}";
    push @lines, "sneakernet version:  $o{sneakernet_version}";
    push @lines, "ZFS_Utils version:   $o{zfsutils_version}";
    push @lines, "Harness version:     $o{harness_version}";
    push @lines, "Execution mode:      $o{mode}";
    push @lines, "--include-optional:  " . ($o{include_optional} ? 'yes' : 'no');
    push @lines, "--baseline-maxdelta: $o{baseline_maxdelta}"
        . ($o{baseline_maxdelta} && $o{baseline_maxdelta} ne '0.9'
           ? '  *** DEVIATION from TESTING.md Step 0.5 - see TESTING_automation.md section 9.2 ***' : '');
    push @lines, '';

    push @lines, '-' x 78;
    push @lines, 'PER-STEP RESULTS';
    push @lines, '-' x 78;
    my %counts;
    for my $r (@{ $o{results} // [] }) {
        $counts{ $r->{status} }++;
        push @lines, sprintf('[%-8s] %-8s %s', $r->{status}, $r->{id}, _step_summary($r));
        if ($r->{status} eq 'N/A' && $r->{note}) {
            push @lines, "             note: $r->{note}";
        }
        if ($r->{error}) {
            push @lines, "             error: " . _first_line($r->{error});
        }
        for my $a (@{ $r->{assertions} // [] }) {
            next if $a->{ok};
            push @lines, "             FAILED: $a->{message}" . ($a->{detail} ? " ($a->{detail})" : '');
        }
    }
    push @lines, '';

    push @lines, '-' x 78;
    push @lines, 'SUMMARY (mirrors TESTING.md\'s own Pass/Fail sheet)';
    push @lines, '-' x 78;
    push @lines, sprintf('%-6s %-70s %s', 'Part', 'What it tests', 'Result');
    for my $partNum (sort { $a <=> $b } keys %{ $o{part_titles} // {} }) {
        my @partResults = grep { $_->{part} == $partNum } @{ $o{results} // [] };
        my $result = _summarize_part(\@partResults);
        push @lines, sprintf('%-6s %-70s %s', $partNum, $o{part_titles}{$partNum}, $result);
    }
    push @lines, '';

    push @lines, '-' x 78;
    push @lines, 'TOTALS';
    push @lines, '-' x 78;
    for my $status (qw(PASS FAIL RECORDED SKIP N/A EXPLAINED)) {
        push @lines, sprintf('  %-10s %d', $status, $counts{$status} // 0);
    }
    push @lines, '';

    if (@{ $o{followups} // [] }) {
        push @lines, '-' x 78;
        push @lines, 'FOLLOW-UP (deliberately not fixed in this pass - see TESTING_automation.md section 13)';
        push @lines, '-' x 78;
        push @lines, "- $_" for @{ $o{followups} };
        push @lines, '';
    }

    return join("\n", @lines) . "\n";
}

sub _step_summary {
    my ($r) = @_;
    return '' unless $r->{assertions};
    my $total = scalar @{ $r->{assertions} };
    my $failed = scalar grep { !$_->{ok} } @{ $r->{assertions} };
    return $total ? "($total assertion" . ($total == 1 ? '' : 's') . ($failed ? ", $failed failed" : ', all passed') . ')' : '';
}

sub _summarize_part {
    my ($results) = @_;
    return 'N/A' unless @$results;
    return 'FAIL' if grep { $_->{status} eq 'FAIL' } @$results;
    return 'PASS (with RECORDED items)' if grep { $_->{status} eq 'RECORDED' } @$results;
    # Checked before the 'all-SKIP'/'all-N/A' tests below: under --explain, a part with an
    # optional step (SKIP, since --include-optional wasn't given) alongside its normal EXPLAINED
    # steps must still summarize as EXPLAINED, not fall through to a misleading bare 'PASS' -
    # nothing in this part actually ran.
    return 'EXPLAINED' if grep { $_->{status} eq 'EXPLAINED' } @$results;
    return 'SKIP' if !grep { $_->{status} ne 'SKIP' } @$results;
    return 'N/A' if !grep { $_->{status} ne 'N/A' } @$results;
    return 'PASS';
}

sub _fmt {
    my ($epoch) = @_;
    return 'n/a' unless defined $epoch;
    return strftime('%Y-%m-%d %H:%M:%S UTC', gmtime($epoch));
}

sub _duration {
    my ($secs) = @_;
    return 'n/a' unless defined $secs;
    my $h = int($secs / 3600); $secs -= $h * 3600;
    my $m = int($secs / 60);   $secs -= $m * 60;
    return sprintf('%dh %dm %ds', $h, $m, $secs);
}

sub _first_line {
    my ($text) = @_;
    return '' unless defined $text;
    my ($line) = split /\n/, $text;
    return $line // '';
}

1;

__END__
See sneakernet/Documentation/TESTING_automation.md section 12. Harness-internal.
