#!/usr/bin/env perl

# Copyright (c) 2025, R. W. Rodolico
# 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.
# Revision History:
# 2026-10-12 Claude v1.0.0 key-for-key tests of the old-key sensors against the v3.7.1 fixture reports (F2a)
# 2026-10-18 Claude bug 210 stage 2: sensor names and package calls follow the unix_* split

use strict;
use warnings;
use Test::More;
use lib './unix';
use YAML::Tiny;

use lib './t';
use fixture_report_helper;

require './unix/sensors/linux_system';
require './unix/sensors/freebsd_system';
require './unix/sensors/linux_network';
require './unix/sensors/freebsd_network';
require './unix/sensors/all_ipmi';
require './unix/sensors/linux_dpkg_software';
require './unix/sensors/freebsd_pkg_software';
require './unix/sensors/linux_virtual';
require './unix/sensors/linux_pci';
require './unix/sensors/freebsd_pci';

my $linux = 't/fixtures/linux-md-lvm';
my $bsd = 't/fixtures/freebsd-zfs';

sub slurp { return fixture_report_helper::slurpFile(shift); }

#######################################################
#
# flatten( $node, $prefix, $out )
#
# Flattens a nested hash into dotted leaf paths and values.
#
# Parameters:
#   $node   - hash reference or scalar
#   $prefix - path so far
#   $out    - hash reference receiving path => value
#
# Returns:
#   $out
#
# Side Effects:
#   mutates $out
#
#######################################################
sub flatten {
   my ($node, $prefix, $out) = @_;
   if (ref($node) eq 'HASH') {
      flatten($node->{$_}, "$prefix\t$_", $out) foreach keys %{$node};
   } else {
      $out->{$prefix} = defined($node) ? $node : '';
   }
   return $out;
}

#######################################################
#
# sectionMatches( $role, $section, $lines, $skip )
#
# Builds a report from sensor lines alone and checks every leaf under
# $section in the v3.7.1 fixture report is present with the same value.
#
# Parameters:
#   $role    - fixture role directory name
#   $section - top-level report section
#   $lines   - array reference of sensor tab lines
#   $skip    - optional regular expression of paths to leave out
#
# Returns:
#   list: number of old keys checked, array reference of problems
#
# Side Effects:
#   none
#
#######################################################
sub sectionMatches {
   my ($role, $section, $lines, $skip) = @_;
   my $old = YAML::Tiny->read("t/fixtures/$role/v3.7.1-report.yaml")->[0];
   my $new = {};
   fixture_report_helper::applyTabLines($new, $lines);
   my $oldFlat = flatten($old->{$section}, $section, {});
   my $newFlat = flatten($new->{$section}, $section, {});
   my @problems;
   my $checked = 0;
   foreach my $path (sort keys %{$oldFlat}) {
      next if defined($skip) && $path =~ /$skip/;
      $checked++;
      if (!exists($newFlat->{$path})) {
         push @problems, "missing $path";
      } elsif ($newFlat->{$path} ne $oldFlat->{$path}) {
         push @problems, "differs $path: old '$oldFlat->{$path}' new '$newFlat->{$path}'";
      }
   }
   return ($checked, \@problems);
}

sub checkSection {
   my ($label, @args) = @_;
   my ($checked, $problems) = sectionMatches(@args);
   ok($checked > 0, "$label: $checked old keys checked");
   is(scalar(@{$problems}), 0, "$label: no missing or changed keys") or diag(join("\n", @{$problems}[0 .. ($#{$problems} < 9 ? $#{$problems} : 9)]));
}

# Linux host
checkSection('linux pci', 'linux-md-lvm', 'pci', linux_pci::parseLspciVmm(slurp("$linux/lspci_-Dvmm.txt")));
checkSection('linux network', 'linux-md-lvm', 'network', [
   @{linux_network::parseLinuxNetwork(slurp("$linux/ip_-o_link.txt"), slurp("$linux/ip_-o_addr.txt"))},
   @{all_ipmi::parseIpmiLan(slurp("$linux/ipmitool_lan_print.txt"))},
]);
checkSection('linux xen/libvirt guests', 'linux-md-lvm', 'xen', linux_virtual::parseVirshList(slurp("$linux/virsh_list.txt")));
checkSection('linux operatingsystem', 'linux-md-lvm', 'operatingsystem', linux_system::parseLinuxOsValues(
   slurp("$linux/lsb_release_-a.txt"), slurp("$linux/uname_-s_-r.txt"), slurp("$linux/cat__proc_version.txt")));
checkSection('linux software', 'linux-md-lvm', 'software', linux_dpkg_software::parseDpkgList(slurp("$linux/dpkg_-l.txt")), 'sysinfo-client');
checkSection('linux ipmi power', 'linux-md-lvm', 'system', all_ipmi::parseIpmiPower(slurp("$linux/ipmitool_sensor.txt")), '^system\t(?!Power)');

# FreeBSD host
checkSection('freebsd network', 'freebsd-zfs', 'network', freebsd_network::parseBsdNetwork(slurp("$bsd/ifconfig_-a.txt")), '^network\tipmi');
checkSection('freebsd operatingsystem', 'freebsd-zfs', 'operatingsystem', freebsd_system::parseBsdOsValues(slurp("$bsd/uname_-s_-r.txt")));
checkSection('freebsd software', 'freebsd-zfs', 'software', freebsd_pkg_software::parsePkgInfo(slurp("$bsd/pkg_info.txt")), 'sysinfo-client');

# FreeBSD pci: F3, a map keyed by slot with no empty key
my $bsdPci = freebsd_pci::parsePciconf(slurp("$bsd/pciconf_-lv.txt"));
ok(@{$bsdPci} > 0, 'freebsd pci has lines');
is(scalar(grep { $_ !~ /^pci\t[^\t]+\t[^\t]+\t.+$/ } @{$bsdPci}), 0, 'freebsd pci lines all have a slot, a key and a value');

# Behaviour fixed by F2a
is_deeply(linux_virtual::parseVirshList(" Id   Name   State\n-----\n 1    a.b   running\n -    c.d   shut off\n"), ["xen\tvirtual\ta.b\tid\t1"], 'virsh: stopped guests skipped');
is_deeply(all_ipmi::parseIpmiPower("Power Meter | 150.000 | Watts | ok\nFan1 | 1 | RPM | ok\n"), ["system\tPower Draw\t150.000 Watts"], 'ipmi: power meter stands alone');
is_deeply(all_ipmi::parseIpmiPower("PS1 Current | 2.0 | Amps | ok\nPS1 Voltage | 100.0 | Volts | ok\nPower Supply 1 | 50 | Watts | ok\n"),
   ["system\tPower PS1 Current\t2.0 Amps", "system\tPower PS1 Voltage\t100.0 Volts", "system\tPower Supply 1 Draw\t50 Watts", "system\tPower Draw\t250 Watts"], 'ipmi: draw = current x voltage + supply watts');

# Disk discovery flags
my $vm = library::discoverDisks({'os' => 'linux', 'lsblkJson' => slurp('t/fixtures/linux-vm/lsblk_-J_-b_-O.txt')});
is($vm->{'vda'}{'transport'}, 'virtio', 'vm disk transport inferred from subsystems');
is($vm->{'vda'}{'is_virtual'}, 1, 'vm disk is_virtual');
my $nas = library::discoverDisks({'os' => 'freebsd', 'kernDisks' => slurp("$bsd/sysctl_-n_kern.disks.txt"), 'geomDisks' => slurp("$bsd/geom_disk_list.txt")});
is($nas->{'da4'}{'transport'}, 'scsi', 'freebsd da disk transport from driver name');
is($nas->{'da4'}{'is_virtual'}, 0, 'physical disk not virtual');
my $nvme = library::discoverDisks({'os' => 'freebsd', 'kernDisks' => slurp('t/fixtures/nvme/sysctl_-n_kern.disks.txt'), 'geomDisks' => slurp('t/fixtures/nvme/geom_disk_list.txt')});
is($nvme->{'nda0'}{'transport'}, 'nvme', 'nda disk is nvme');
my $md = library::discoverDisks({'os' => 'linux', 'lsblkJson' => slurp("$linux/lsblk_-J_-b_-O.txt")});
is($md->{'sdc'}{'rotation_rpm'}, 0, 'non-rotating linux disk has rotation_rpm 0');

done_testing();
