#!/usr/bin/perl
# t/mutant_killers.t -- tests designed to kill surviving mutants from
# xt/mutant_20260611_120738.t (generated by App::Test::Generator).
#
# Each subtest targets a specific mutation and contains assertions that:
#   - PASS with the original code
#   - FAIL with the mutated code
#
# Mutants targeted:
#   COND_INV_342_2  -- new():              if(ref) -> unless(ref)
#   COND_INV_352_2  -- new():              unless(bypass) -> if(bypass)
#   COND_INV_414_2  -- check_abstract():   if(ref) -> unless(ref)
#   COND_INV_423_2  -- check_abstract():   unless(bypass) -> if(bypass)
#   BOOL_NEGATE_517_2 -- _is_direct_abstract(): return 0 -> return 1  (undef case)
#   BOOL_NEGATE_520_2 -- _is_direct_abstract(): return 1 -> return 0  ($SELF case)
#   RETURN_UNDEF_517_2 -- _is_direct_abstract(): return 0 -> return undef (undef case)
#   RETURN_UNDEF_520_2 -- _is_direct_abstract(): return 1 -> return undef ($SELF case)

use strict;
use warnings;

use Readonly;
use Scalar::Util qw(blessed);
use Test::Most;
use Test::Returns;

use Class::Abstract;

# ---------------------------------------------------------------------------
# Configuration -- all constants in one place to avoid magic strings
# ---------------------------------------------------------------------------

my %config = (
	# Fixture package names (MK:: prefix avoids collision with other test files)
	pkg_abstract => 'MK::Abstract',
	pkg_concrete => 'MK::Concrete',
	pkg_module   => 'Class::Abstract',

	# Exact error patterns from the module POD
	err_abstract     => qr/Cannot instantiate abstract class \S+ directly/,
	err_new_unblessed => qr/new\(\) invocant must be a class name or blessed object, got/,
	err_chk_unblessed => qr/check_abstract\(\) requires a class name or blessed object/,

	# Test::Returns schemas for exact return type verification
	schema_integer => { type => 'integer' },
	schema_undef   => { type => 'undef'   },
);

# Exact integer values -- using named constants to make intent clear
Readonly::Scalar my $ZERO => 0;
Readonly::Scalar my $ONE  => 1;

# ---------------------------------------------------------------------------
# Fixture packages
# ---------------------------------------------------------------------------

# MK::Abstract: directly abstract via use parent
{
	package MK::Abstract;
	use parent -norequire, 'Class::Abstract';
}

# MK::Concrete: concrete subclass -- inherits via @ISA, provides own new()
{
	package MK::Concrete;
	our @ISA = ('MK::Abstract');
	sub new { my ($class) = @_; return $class->SUPER::new() }
}

diag 'Mutant killer tests for Class::Abstract' if $ENV{TEST_VERBOSE};

# ---------------------------------------------------------------------------
# Helper: disable all bypass paths so enforcement fires inside a harness
# ---------------------------------------------------------------------------

sub enforcement_on (&) {
	my ($code) = @_;
	local $Class::Abstract::BYPASS                 = 0;
	local $Class::Abstract::config{harness_bypass} = 0;
	local $ENV{HARNESS_ACTIVE}                     = 0;
	return $code->();
}

# ===========================================================================
# MUTANT: COND_INV_342_2
#
# Source:   if (ref $class) { croak unless blessed; $class = ref($class) }
# Mutation: unless (ref $class) { ... }   <-- condition INVERTED
#
# Effect of mutation:
#   The ref-check block fires for NON-references (plain strings) instead of
#   references.  For a plain string like 'MK::Concrete':
#     - blessed('MK::Concrete') = undef -> CROAK (wrong!)
#   For an unblessed ref like []:
#     - Block skipped; falls through without the "got ARRAY" croak (wrong!)
#
# Killer strategy:
#   1. A class-name string as invocant must SUCCEED (mutant would croak it).
#   2. An unblessed ref must croak with the exact "got TYPE" message
#      (mutant would skip the message and either succeed or give a different error).
# ===========================================================================

subtest 'COND_INV_342_2 killer -- new(): if(ref) vs unless(ref)' => sub {
	plan tests => 5;

	diag 'Testing ref-condition direction in new()' if $ENV{TEST_VERBOSE};

	# --- Killer 1: class-name string invocant must SUCCEED ---
	# With original (if ref): ref('MK::Concrete') = '' -> condition FALSE -> skip block -> ok
	# With mutant (unless ref): ref('MK::Concrete') = '' -> condition TRUE -> blessed fails -> CROAK
	my $obj;
	lives_ok { $obj = MK::Concrete->new() }
		'COND_INV_342_2: class-name string invocant lives (mutant would croak it)';

	ok blessed($obj), 'returned object is blessed (mutation would have prevented this)';

	is ref($obj), $config{pkg_concrete},
		'object is blessed into the correct concrete class';

	# --- Killer 2: unblessed ref invocant must croak with exact "got TYPE" message ---
	# With original (if ref): ref([]) = 'ARRAY' -> condition TRUE -> blessed fails -> CROAK "got ARRAY"
	# With mutant (unless ref): ref([]) = 'ARRAY' -> condition FALSE -> block skipped -> no "got ARRAY" croak
	throws_ok { Class::Abstract::new([]) }
		qr/new\(\) invocant must be a class name or blessed object, got ARRAY/,
		'COND_INV_342_2: unblessed ref croaks with exact "got ARRAY" message (mutant skips this)';

	# Also verify a HASH ref gives "got HASH" (not "got ARRAY") to confirm type-name precision
	throws_ok { Class::Abstract::new({}) }
		qr/new\(\) invocant must be a class name or blessed object, got HASH/,
		'COND_INV_342_2: unblessed hashref croaks with exact "got HASH" message';
};

# ===========================================================================
# MUTANT: COND_INV_352_2
#
# Source:   unless ($BYPASS || ($config{harness_bypass} && $ENV{HARNESS_ACTIVE})) {
#               croak if _is_direct_abstract($class);
#           }
# Mutation: if ($BYPASS || ...) { croak if ... }   <-- unless INVERTED to if
#
# Effect of mutation:
#   Enforcement fires WHEN bypass is active, and is SUPPRESSED when no bypass.
#   - $BYPASS=1 -> enforcement FIRES (wrong! should bypass)
#   - No bypass  -> enforcement SUPPRESSED (wrong! should fire)
#
# Killer strategy:
#   1. BYPASS=1 on abstract class -> lives (mutant would make it croak)
#   2. No bypass, abstract class  -> croaks (mutant would make it live)
# ===========================================================================

subtest 'COND_INV_352_2 killer -- new(): unless(bypass) vs if(bypass)' => sub {
	plan tests => 3;

	diag 'Testing bypass-condition direction in new()' if $ENV{TEST_VERBOSE};

	# --- Killer 1: $BYPASS=1 must SUPPRESS the croak ---
	# Original (unless bypass): bypass active -> condition FALSE -> skip croak -> LIVES
	# Mutant (if bypass):        bypass active -> condition TRUE  -> CROAK (wrong!)
	{
		local $Class::Abstract::BYPASS                 = 1;
		local $Class::Abstract::config{harness_bypass} = 0;
		local $ENV{HARNESS_ACTIVE}                     = 0;

		my $obj;
		lives_ok { $obj = MK::Abstract->new() }
			'COND_INV_352_2: BYPASS=1 suppresses croak (mutant would fire it)';
	}

	# --- Killer 2: no bypass must FIRE the croak ---
	# Original (unless bypass): bypass inactive -> condition TRUE -> CROAK
	# Mutant (if bypass):        bypass inactive -> condition FALSE -> no croak (wrong!)
	enforcement_on {
		throws_ok { MK::Abstract->new() }
			$config{err_abstract},
			'COND_INV_352_2: no bypass fires enforcement (mutant would suppress it)';
	};

	# --- Killer 3: concrete class must still succeed (bypass direction only affects abstract) ---
	enforcement_on {
		lives_ok { MK::Concrete->new() }
			'COND_INV_352_2: concrete class unaffected regardless of bypass direction';
	};
};

# ===========================================================================
# MUTANT: COND_INV_414_2
#
# Source:   if (ref $class) { croak unless blessed; $class = ref($class) }
#           in check_abstract()
# Mutation: unless (ref $class) { ... }   <-- condition INVERTED
#
# Same analysis as COND_INV_342_2 but applied to check_abstract().
# Killer strategy: mirror the new() killers.
# ===========================================================================

subtest 'COND_INV_414_2 killer -- check_abstract(): if(ref) vs unless(ref)' => sub {
	plan tests => 4;

	diag 'Testing ref-condition direction in check_abstract()' if $ENV{TEST_VERBOSE};

	# --- Killer 1: class-name string must NOT produce the "unblessed" croak ---
	# Original (if ref): ref('MK::Concrete') = '' -> skip block -> no wrong croak
	# Mutant (unless ref): ref('MK::Concrete') = '' -> enter block -> blessed fails -> CROAK
	enforcement_on {
		lives_ok { Class::Abstract::check_abstract($config{pkg_concrete}) }
			'COND_INV_414_2: class-name string invocant lives (mutant would croak it)';
	};

	# --- Killer 2: unblessed ref must croak with exact error ---
	# Original (if ref): ref([]) = 'ARRAY' -> TRUE -> enters block -> blessed fails -> CROAK
	# Mutant (unless ref): ref([]) = 'ARRAY' -> FALSE -> skips block -> no "unblessed" croak
	throws_ok { Class::Abstract::check_abstract([]) }
		$config{err_chk_unblessed},
		'COND_INV_414_2: unblessed arrayref croaks with documented error (mutant skips this)';

	throws_ok { Class::Abstract::check_abstract({}) }
		$config{err_chk_unblessed},
		'COND_INV_414_2: unblessed hashref also croaks with documented error';

	# --- Killer 3: blessed concrete object invocant must NOT croak ---
	# If the ref check is inverted, a blessed object (which IS a ref) would SKIP the
	# block, and its string form would hit the abstract check instead.
	my $concrete_obj = MK::Concrete->new();
	enforcement_on {
		lives_ok { Class::Abstract::check_abstract($concrete_obj) }
			'COND_INV_414_2: blessed concrete object passes (mutant would skip class extraction)';
	};
};

# ===========================================================================
# MUTANT: COND_INV_423_2
#
# Source:   unless ($BYPASS || (...)) { croak if _is_direct_abstract($class) }
#           in check_abstract()
# Mutation: if ($BYPASS || ...) { ... }   <-- unless INVERTED to if
#
# Same analysis as COND_INV_352_2 but applied to check_abstract().
# ===========================================================================

subtest 'COND_INV_423_2 killer -- check_abstract(): unless(bypass) vs if(bypass)' => sub {
	plan tests => 2;

	diag 'Testing bypass-condition direction in check_abstract()' if $ENV{TEST_VERBOSE};

	# --- Killer 1: BYPASS=1 must SUPPRESS the croak ---
	{
		local $Class::Abstract::BYPASS                 = 1;
		local $Class::Abstract::config{harness_bypass} = 0;
		local $ENV{HARNESS_ACTIVE}                     = 0;

		lives_ok { Class::Abstract::check_abstract($config{pkg_abstract}) }
			'COND_INV_423_2: BYPASS=1 suppresses croak (mutant would fire it)';
	}

	# --- Killer 2: no bypass must FIRE the croak ---
	enforcement_on {
		throws_ok { Class::Abstract::check_abstract($config{pkg_abstract}) }
			$config{err_abstract},
			'COND_INV_423_2: no bypass fires enforcement (mutant would suppress it)';
	};
};

# ===========================================================================
# MUTANTS: BOOL_NEGATE_517_2 and RETURN_UNDEF_517_2
#
# Source:   return 0 unless defined $class;   (line 517 in _is_direct_abstract)
#
# BOOL_NEGATE_517_2 -- return 0 changed to: return 1
#   Effect: _is_direct_abstract(undef) returns 1 instead of 0.
#   Consequence: new(undef) would bypass the defined-guard and hit the
#   abstract check instead, croaking with the WRONG error message.
#
# RETURN_UNDEF_517_2 -- return 0 changed to: return undef
#   Effect: _is_direct_abstract(undef) returns undef instead of 0.
#   Consequence: 'if undef' is still false so enforcement doesn't fire,
#   but the exact return value changes from 0 to undef.
#
# Killer strategy: call the private helper directly with undef and assert
# the return value is EXACTLY the integer 0 (not 1, not undef).
# ===========================================================================

subtest 'BOOL_NEGATE_517_2 + RETURN_UNDEF_517_2 killers -- _is_direct_abstract(undef) = 0' => sub {
	plan tests => 3;

	diag '_is_direct_abstract(undef) must return exactly 0' if $ENV{TEST_VERBOSE};

	my $result = Class::Abstract::_is_direct_abstract(undef);

	diag "result = " . (defined $result ? $result : 'undef') if $ENV{TEST_VERBOSE};

	# BOOL_NEGATE: if return 1, is(1,0) fails -- kills the mutant
	# RETURN_UNDEF: if return undef, is('',0) ('' ne '0') fails -- kills the mutant
	is $result, $ZERO,
		'_is_direct_abstract(undef) returns exactly 0 (not 1, not undef)';

	# Verify integer type via Test::Returns (catches undef vs 0 difference)
	returns_ok $result, $config{schema_integer},
		'_is_direct_abstract(undef) satisfies integer return schema';

	# Confirm it is defined (undef return would fail this)
	ok defined($result),
		'_is_direct_abstract(undef) return value is defined (not undef)';
};

# ===========================================================================
# MUTANTS: BOOL_NEGATE_520_2 and RETURN_UNDEF_520_2
#
# Source:   return 1 if $class eq $SELF;   (line 520 in _is_direct_abstract)
#
# BOOL_NEGATE_520_2 -- return 1 changed to: return 0
#   Effect: _is_direct_abstract('Class::Abstract') returns 0 instead of 1.
#   Consequence: Class::Abstract itself could be instantiated directly!
#
# RETURN_UNDEF_520_2 -- return 1 changed to: return undef
#   Effect: _is_direct_abstract('Class::Abstract') returns undef instead of 1.
#   Consequence: 'if undef' is false, so enforcement does not fire for $SELF.
#
# Killer strategy:
#   1. _is_direct_abstract('Class::Abstract') must return exactly 1.
#   2. new('Class::Abstract') with enforcement on must croak.
# ===========================================================================

subtest 'BOOL_NEGATE_520_2 + RETURN_UNDEF_520_2 killers -- _is_direct_abstract(Class::Abstract) = 1' => sub {
	plan tests => 4;

	diag '_is_direct_abstract("Class::Abstract") must return exactly 1' if $ENV{TEST_VERBOSE};

	my $result = Class::Abstract::_is_direct_abstract($config{pkg_module});

	diag "result = " . (defined $result ? $result : 'undef') if $ENV{TEST_VERBOSE};

	# BOOL_NEGATE: if return 0, is(0,1) fails -- kills the mutant
	# RETURN_UNDEF: if return undef, is('',1) ('' ne '1') fails -- kills the mutant
	is $result, $ONE,
		'_is_direct_abstract("Class::Abstract") returns exactly 1 (not 0, not undef)';

	# Integer schema confirms return is a proper integer, not undef
	returns_ok $result, $config{schema_integer},
		'_is_direct_abstract("Class::Abstract") satisfies integer return schema';

	ok defined($result),
		'_is_direct_abstract("Class::Abstract") is defined (not undef)';

	# End-to-end: Class::Abstract itself must NOT be directly instantiable.
	# With BOOL_NEGATE or RETURN_UNDEF, _is_direct_abstract returns falsy for $SELF,
	# so the croak is never reached and this test would PASS (lives) when it should FAIL.
	enforcement_on {
		throws_ok { Class::Abstract::new($config{pkg_module}) }
			qr/Cannot instantiate abstract class Class::Abstract directly/,
			'new("Class::Abstract") croaks (mutant 0/undef return would suppress this)';
	};
};

# ===========================================================================
# COMBINED REGRESSION: all mutants at once
#
# A final integration subtest that exercises the full call chain end-to-end.
# If any mutant above survived, at least one of these assertions would fail.
# ===========================================================================

subtest 'combined regression -- all mutants, full call chain' => sub {
	plan tests => 6;

	diag 'Regression: combining all mutant killer scenarios' if $ENV{TEST_VERBOSE};

	# 1. String invocant on concrete class must succeed (COND_INV_342 + 414 killers)
	lives_ok { MK::Concrete->new() }
		'concrete class-name invocant succeeds (kills COND_INV_342_2 and 414_2 if mutated)';

	# 2. Unblessed ref must croak (COND_INV_342 killer)
	throws_ok { Class::Abstract::new([]) }
		$config{err_new_unblessed},
		'unblessed ref to new() croaks (kills COND_INV_342_2 if mutated)';

	# 3. BYPASS=1 suppresses enforcement (COND_INV_352 + 423 killers)
	{
		local $Class::Abstract::BYPASS                 = 1;
		local $Class::Abstract::config{harness_bypass} = 0;
		local $ENV{HARNESS_ACTIVE}                     = 0;
		lives_ok { MK::Abstract->new() }
			'BYPASS=1 suppresses enforcement (kills COND_INV_352_2 and 423_2 if mutated)';
	}

	# 4. No bypass fires enforcement (COND_INV_352 + 423 killers)
	enforcement_on {
		throws_ok { MK::Abstract->new() }
			$config{err_abstract},
			'no bypass fires enforcement (kills COND_INV_352_2 and 423_2 if mutated)';
	};

	# 5. _is_direct_abstract(undef) = 0, not 1 or undef (BOOL_NEGATE/RETURN_UNDEF 517 killers)
	is Class::Abstract::_is_direct_abstract(undef), $ZERO,
		'_is_direct_abstract(undef) is 0 (kills BOOL_NEGATE_517_2 and RETURN_UNDEF_517_2)';

	# 6. _is_direct_abstract('Class::Abstract') = 1, not 0 or undef (BOOL_NEGATE/RETURN_UNDEF 520 killers)
	is Class::Abstract::_is_direct_abstract($config{pkg_module}), $ONE,
		'_is_direct_abstract("Class::Abstract") is 1 (kills BOOL_NEGATE_520_2 and RETURN_UNDEF_520_2)';
};

done_testing;
