package App::Test::Generator::Template;

use strict;
use warnings;
use autodie qw(:all);

use utf8;
use Data::Section::Simple;

our $VERSION = '0.46';

=head1 NAME

App::Test::Generator::Template - Template for the test files generated by App::Test::Generator

=head1 VERSION

Version 0.46

=head1 SYNOPSIS

The template for the test files generated by App::Test::Generator.

=head1 METHODS

=head2 get_data_section

Returns a reference to the named template, read from this module's
C<__DATA__> section via L<Data::Section::Simple>.

    my $template_ref = App::Test::Generator::Template->get_data_section('test.tt');
    my $template_ref = App::Test::Generator::Template::get_data_section('test.tt');

=head3 Arguments

=over 4

=item * C<$template_file>

The name of the C<@@>-delimited section to retrieve. The only value
currently defined in this module's C<__DATA__> section is C<test.tt>.
May be called either as a class method (C<< Template->get_data_section(...) >>,
in which case the leading class-name argument is stripped before lookup)
or as a plain function.

=back

=head3 Returns

A scalar reference to the template content. If C<$template_file> does
not match a defined C<@@> section, returns a reference to C<undef>.

=head3 Side Effects

None. Does not write to disk or modify any global state.

=head3 API specification

=head4 input

    {
        template_file => { type => SCALAR, optional => 1 },
    }

=head4 output

    { type => SCALARREF }

=cut

sub get_data_section
{
	if($_[0] && ($_[0] eq __PACKAGE__)) {
		shift;
	}

	return \Data::Section::Simple::get_data_section($_[0]);
}

1;

=head1 AUTHOR

Nigel Horne, C<< <njh at nigelhorne.com> >>

Portions of this module's initial design and documentation were created with the
assistance of L<ChatGPT|https://openai.com/> (GPT-5), with final curation
and authorship by Nigel Horne.

=cut

__DATA__

@@ test.tt
#!/usr/bin/env perl

use strict;
use warnings;

use utf8;
use open qw(:std :encoding(UTF-8));	# https://github.com/nigelhorne/App-Test-Generator/issues/1

use Carp;
use Data::Dumper;
use Data::Random qw(:all);
use Data::Random::String;
use Data::Random::String::Matches 0.02;
use Data::Random::Structure;
use Encode qw(decode_utf8);
use Unicode::Normalize ();
use Test::Most;
use Test::Returns 0.02;
use Unicode::GCString;

[% setup_code %]

[% IF module %]
	diag('[% module %]->[% function %] test case created by https://github.com/nigelhorne/App-Test-Generator');
[% ELSE %]
	diag('[% function %] test case created by https://github.com/nigelhorne/App-Test-Generator');
[% END %]

die_on_fail if($ENV{'DIE_ON_FAIL'});

# Edge-case maps injected from config (optional)
my %edge_cases = (
[% edge_cases_code %]
);
my @edge_case_array = (
[% edge_case_array_code %]
);
my %type_edge_cases = (
[% type_edge_cases_code %]
);
my %config = (
[% config_code %]
);

if($^O ne 'MSWin32' && $config{close_stdin}) {

	close(STDIN);
	open(STDIN, '<', '/dev/null');
}

# TODO: add more, and remove magic numbers
use constant {
	PROB_LOWERCASE => $config{prob_lowercase} // 0.72,
	PROB_EDGE_CASE => $config{prob_edge_case} // 0.4,
};

# Seed for reproducible fuzzing (if provided)
[% seed_code %]

my %input = (
[% input_code %]
);

my %output = (
[% output_code %]
);

my %transforms = (
[% transforms_code %]
);

my @relationships = (
[% relationships_code %]
);

# Candidates for regex comparisons
my @candidate_good = ('123', 'abc', 'A1B2', '0');
my @candidate_bad = (
	"😊",	# emoji
	"１２３",	# full-width digits
	"١٢٣",	# Arabic digits
	'..',	# regex metachars
	"a\nb",	# newline in middle
	"é",	# E acute
	'x' x 5000,	# huge string
	*STDOUT,
	' ',	# space
	"\t",	# tab
	"\r",	# carriage return

	# Added later if the configuration says so
	# '',	# empty
	# undef,	# undefined
	# "\0",	# null byte
);
my $positions = populate_positions(\%input);

# --- Fuzzer helpers ---
sub _pick_from {
	my $arrayref = $_[0];
	return undef unless $arrayref && ref $arrayref eq 'ARRAY' && @{$arrayref};
	return $arrayref->[ int(rand(scalar @$arrayref)) ];
}

sub rand_ascii_str {
	my $len = shift // int(rand(10)) + 1;
	# join '', map { chr(97 + int(rand(26))) } 1..$len;
	return Data::Random::String->create_random_string(length => $len, contains => 'alphanumeric');
}

my @unicode_codepoints = (
	0x00A9,	# ©
	0x00AE,	# ®
	0x03A9,	# Ω
	0x20AC,	# €
	0x2013,	# – (en-dash)
	0x0301,	# combining acute accent
	0x0308,	# combining diaeresis
	0x1F600,	# 😀 (emoji)
	0x1F62E,	# 😮
	0x1F4A9,	# 💩 (yes)
);

# Tests for matches or nomatch
my @regex_tests = (
	'match123',
	'nope',
	'/fullpath',
	'/',
	'/etc/passwd',
	'../../etc/passwd',
	"/etc/passwd\0",
	"D:\\dos_path",
	"I:\\",
	'/(?{ exit 1 })/',
);

# unified generator to randomly produces codepoint strings,
#	grapheme clusters, ZWJ emoji sequences, or aggressive Unicode fuzz strings
sub rand_str
{
	my $len = $_[0];
	if(!defined($len)) {
		$len = int(rand(10)) + 1;	# length random number between 1 and 10
	}

	return '' if($len == 0);

	if(!($config{'test_non_ascii'} // 0)) {
		return rand_ascii_str($len);
	}

	# my $rc = _rand_str_basic($len);
	# $rc = _rand_unicode_fuzzer($len);
	# my $l = Unicode::GCString->new($rc)->length();
	# if($len > $l) {
		# $rc .= 'a' x ($len - $l);	# Why is this needed?
	# }

	# return $rc;

	# TODO: length issues at the moment
	my $mode = int(rand(5));	# 0..4

	my $rc = _rand_str_basic($len);

	$rc = _rand_str_basic($len) if $mode == 0;
	$rc = _rand_codepoint_exact($len) if $mode == 1;
	$rc = _rand_grapheme_exact($len) if $mode == 2;
	$rc = _rand_unicode_fuzzer($len) if $mode == 3;
	$rc = rand_ascii_str($len) if($mode == 4);

	my $rc_len = Unicode::GCString->new($rc)->length();
	if($rc_len > $len) {
		my $gcstr = Unicode::GCString->new($rc);
		$rc = $gcstr->substr(0, $len)->as_string();
		$rc_len = Unicode::GCString->new($rc)->length();
	}
	if($len > $rc_len) {
		$rc .= 'a' x ($len - $rc_len);
		$rc_len = Unicode::GCString->new($rc)->length();
	}

	fail("BUG $rc_len != $len (mode == $mode)") if($rc_len != $len);

	return $rc;
}

#####################################################
# 1. EXACT-LENGTH CODEPOINT MODE
# Generate a random string: mostly ASCII, sometimes unicode, sometimes nul bytes or combining marks
#####################################################
sub _rand_str_basic
{
	my $len = $_[0];

	my @chars;
	for (1..$len) {
		my $r = rand();
		if ($r < PROB_LOWERCASE) {
			push @chars, chr(97 + int(rand(26)));	# a-z
		} elsif ($r < 0.88) {
			push @chars, chr(65 + int(rand(26)));	# A-Z
		} elsif ($r < 0.95) {
			push @chars, chr(48 + int(rand(10)));	# 0-9
		} elsif($r < 0.975) {
			push @chars, _rand_unicode_char();	# occasional emoji/marks
		} elsif($config{'test_nuls'}) {
			push @chars, chr(0);	# nul byte injection
		} else {
			push @chars, chr(97 + int(rand(26)));	# a-z
		}
	}

	if (rand() < 0.08) {
		# 8% chance to prepend combining acute accent (0301)
		$chars[-1] = chr(0x0301);
	} elsif (rand() < 0.08) {
		# 8% chance to append combining diaeresis (0308)
		$chars[-1] .= chr(0x0308);
	}
	return join('', @chars);
}

#####################################################
# 2. EXACT-LENGTH CODEPOINT MODE
# combining marks decorate characters
#####################################################
sub _rand_codepoint_exact {
	my $len = $_[0];
	my @chars;

	for (1..$len) {
		my $c = _rand_base_char();
		if(rand() < 0.08) {
			# prepend combining acute
			$c = chr(0x0301)
		} elsif(rand() < 0.08) {
			# append combining dieresis
			$c = chr(0x0308);
		}
		push @chars, $c;
	}

	return join('', @chars);
}

# helper for codepoint mode
sub _rand_base_char {
	my $r = rand();

	if ($r < 0.70) { return chr(97 + int(rand(26))); }
	if ($r < 0.88) { return chr(65 + int(rand(26))); }
	if ($r < 0.95) { return chr(48 + int(rand(10))); }
	return _rand_unicode_char();
}

#####################################################
# 3. EXACT-LENGTH GRAPHEME-CLUSTER MODE
# each "character" is a whole grapheme cluster:
# - emoji with ZWJ sequences
# - flags
# - skin-tone variants
# - accented characters
#####################################################
sub _rand_grapheme_exact {
	my $len = $_[0];
	my @clusters;

	my @emoji_base = qw(
		😀 😁 😂 🤣 😅 😊 😎 😍 😡 🥳
		👍 👎 👋 🖐 🙏 💩 🧠 ❤️ 🫠
	);

	my @skin_tones = (
		"\x{1F3FB}", "\x{1F3FC}", "\x{1F3FD}", "\x{1F3FE}", "\x{1F3FF}"
	);

	my @zwj_parts = (
		"\x{200D}\x{1F33A}",	# ZWJ + Flower
		"\x{200D}\x{1F4BB}",	# ZWJ + Laptop
		"\x{200D}\x{1F9D1}",	# ZWJ + person
	);

	my @flags = (
		"\x{1F1FA}\x{1F1F8}", # US
		"\x{1F1EC}\x{1F1E7}", # UK
		"\x{1F1E8}\x{1F1E6}", # CA
		"\x{1F1E6}\x{1F1FA}", # AU
	);

	for (1..$len) {
		my $type = rand();

		if ($type < 0.4) {
			# base emoji
			my $e = $emoji_base[ rand @emoji_base ];

			# TODO
			# maybe add skin tone
			# $e .= $skin_tones[rand @skin_tones] if rand() < 0.3;

			# TODO
			# maybe add zwj sequence
			# $e .= $zwj_parts[rand @zwj_parts] if rand() < 0.15;

			push @clusters, $e;
		} elsif ($type < 0.55) {
			# flag (always 1 grapheme cluster)
			push @clusters, $flags[rand @flags];
		} elsif ($type < 0.75) {
			# accented letter (composed or decomposed)
			my $base = chr(97 + int(rand(26))); # a-z
			my $accented = $base . chr(0x0301);
			$accented = Unicode::Normalize::NFC($accented) if rand() < 0.5;
			push @clusters, $accented;
		} else {
			# fallback ASCII
			push @clusters, chr(97 + int(rand(26)));
		}
	}

	return join('', @clusters);
}

####################################################
# 4. UNICODE FUZZER MODE
# Extremely aggressive: invalid sequences, NULs, bidirectional markers, Zalgo
# Exclude unpaired surrogates for now, since TAP::Harness complains about that
####################################################
sub _rand_unicode_fuzzer {
	my $len = $_[0];
	my @out;

	my @zalgo_up = map { chr($_) } (0x030D..0x036F);
	my @bidi = ("\x{202A}", "\x{202B}", "\x{202D}", "\x{202E}", "\x{2066}", "\x{2067}");
	my @weird = $config{'test_nuls'} ? ("\x{0000}", "\x{FFFD}", "\x{FEFF}") : ("\x{FFFD}", "\x{FEFF}");

	for (1..$len) {
		my $r = rand();

		if ($r < 0.25) {
			# push @out, chr( int(rand(0x10FFFF)) );	# random codepoint will include surragates
			# Generate random codepoint, excluding surrogate range
			my $cp;
			do {
				$cp = int(rand(0x10FFFF));
			} while ($cp >= 0xD800 && $cp <= 0xDFFF);
			push @out, chr($cp);
		} elsif ($r < 0.40) {
			push @out, chr(65 + int(rand(26))) . $zalgo_up[rand @zalgo_up]; # Zalgo
		} elsif ($r < 0.55) {
			push @out, $bidi[rand @bidi];
		} elsif ($r < 0.70) {
			push @out, $weird[rand @weird];
		} else {
			push @out, _rand_unicode_char();
		}
	}

	return join('', @out);
}

###################################################
# Random Unicode character helper
###################################################
sub _rand_unicode_char
{
	if(rand() < 0.5) {
		my $cp = $unicode_codepoints[ int(rand(@unicode_codepoints)) ];
		return chr($cp);
	}

	my @pool = (
		0x00A9, 0x00AE, 0x2600, 0x2601,
		0x1F600 + int(rand(200)),	# emoji block
		0x0300 + int(rand(80)),	# combining marks
	);
	return chr( $pool[int rand @pool] );
}

# Random character either upper or lower case
# sub rand_char
# {
	# return rand_chars(set => 'all', min => 1, max => 1);

	# my $char = '';
	# my $upper_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
	# my $lower_chars = 'abcdefghijklmnopqrstuvwxyz';
	# my $combined_chars = $upper_chars . $lower_chars;

	# # Generate a random index between 0 and the length of the string minus 1
	# my $rand_index = int(rand(length($combined_chars)));

	# # Get the character at that index
	# return substr($combined_chars, $rand_index, 1);
# }

# Integer generator: mix typical small ints with large limits
sub rand_int {
	my $r = rand();
	if ($r < 0.75) {
		return int(rand(200)) - 100;	# -100 .. 100 (usual)
	} elsif ($r < 0.9) {
		return int(rand(2**31)) - 2**30;	# 32-bit-ish
	} elsif ($r < 0.98) {
		return (int(rand(2**63)) - 2**62);	# 64-bit-ish
	} else {
		# very large/suspicious values
		return 2**63 - 1;
	}
}
sub rand_bool { rand() > 0.5 ? 1 : 0 }

# Number generator (floating), includes tiny/huge floats
sub rand_num {
	my $r = rand();
	if ($r < 0.7) {
		return (rand() * 200 - 100);	# -100 .. 100
	} elsif ($r < 0.9) {
		return (rand() * 1e12) - 5e11;	# large-ish
	} elsif ($r < 0.95) {
		return -0.0;	# Negative 0
	} elsif ($r < 0.96) {
		return (rand() * 1e308) - 5e307;	# very large floats
	} elsif($r < 0.97) {
		return 9**9**9;	# Infinity
	} else {
		return 1e-308 * (rand() * 1000);	# tiny float, subnormal-like
	}
}

sub rand_arrayref {
	my $len = shift || int(rand(3)) + 1; # small arrays

	return Data::Random::Structure->new(max_elements => $len, max_depth => 1)->generate_array();

	# return [ map { rand_str() } 1..$len ];
}

sub rand_hashref {
	my $len = shift || int(rand(3)) + 1; # small hashes

	return Data::Random::Structure->new(max_elements => $len, max_depth => 1)->generate_hash();

	# my %h;
	# for (1..$len) {
		# $h{rand_str(3)} = rand_str(5);
	# }
	# return \%h;
}

sub rand_email
{
	my $len = shift || int(rand(10));
	my $l;
	my @name;
	my @tlds = qw(com org net edu gov io co uk de fr);

	for($l = 0; $l < $len; $l++) {
		push @name, pack('c', (int(rand 26))+97);
	}
	push @name, '@';
	$len = rand(10);
	for($l = 0; $l < $len; $l++) {
		push @name, pack('c', (int(rand 26))+97);
	}
	push @name, '.';
	$len = rand($#tlds+1);
	push @name, $tlds[$len];
	return join('', @name);
}

sub fuzz_inputs
{
	my @cases;

	# Are any options mandatory?
	my $all_optional = 1;
	my %mandatory_strings;	# List of mandatory strings to be added to all tests, always put at start so it can be overwritten
	my %mandatory_objects;
	my %mandatory_numbers;
	my $class_simple_loaded;

	# Create a hash ref of the mandatory args
	foreach my $field (keys %input) {
		my $spec = $input{$field} || {};
		if((ref($spec) eq 'HASH') && (!$spec->{optional})) {
			$all_optional = 0;
			if($spec->{'type'} eq 'string') {
				local $config{'test_undef'} = 0;
				local $config{'test_nuls'} = 0;
				local $config{'test_empty'} = 0;
				if($spec->{'matches'}) {
					$mandatory_strings{$field} = Data::Random::String::Matches->create_random_string({ regex => $spec->{'matches'} });
				} elsif(defined $spec->{memberof}) {
					my @set = @{$spec->{memberof}};
					$mandatory_strings{$field} = $set[-1];
				} else {
					$mandatory_strings{$field} = rand_ascii_str($spec->{max} // $spec->{min});
				}
			} elsif($spec->{'type'} eq 'object') {
				my $method = $spec->{'can'};
				if(!defined($method)) {
					die "$field: type is object, but 'can' is not specified which is needed to mock the object";
				}
				if(!$class_simple_loaded) {
					require_ok('Class::Simple');
					eval {
						Class::Simple->import();
						$class_simple_loaded = 1;
					};
				}
				my $simple_obj = new_ok('Class::Simple');
				$simple_obj->$method(1);
				$mandatory_objects{$field} = $simple_obj;
			} elsif(($spec->{'type'} eq 'float') || ($spec->{'type'} eq 'number') || ($spec->{'type'} eq 'integer')) {
				my $number;
				if(defined(my $min = $spec->{min})) {
					$number = rand($min);
				} else {
					$number = rand(100000);
				}
				if(defined(my $max = $spec->{max})) {
					if($number > $max) {
						$number = $max;
					}
				}
				if($spec->{'type'} eq 'integer') {
					$number = int($number);
				}
				$mandatory_numbers{$field} = $number;
			} elsif($spec->{type} eq 'hashref') {
				if(defined($spec->{schema})) {
					die __PACKAGE__, ': TODO: add schema support to hashref';
				} elsif($spec->{max} || $spec->{min}) {
					die __PACKAGE__, ': TODO: add size support to hashref';
				}
				$mandatory_objects{$field} = { 'line' => __LINE__ };
			} elsif(($spec->{type} eq 'arrayref') || ($spec->{type} eq 'array')) {
				$mandatory_objects{$field} = [];
			} elsif($spec->{type} eq 'any') {
				$mandatory_strings{$field} = rand_ascii_str(8);
			} else {
				die __PACKAGE__, ': TODO: type = ', $spec->{'type'};
			}
		}
	}

	# Don't do for now, until transform object code is added
	# my %mandatory_args;
	# if(my $foundation = _fill_foundation()) {
		# %mandatory_args = %{$foundation};
	# }
	my %mandatory_args = (%mandatory_strings, %mandatory_objects, %mandatory_numbers);

	if(($all_optional) || ((scalar keys %input) > 1)) {
		# Basic test cases
		if(((scalar keys %input) == 1) && exists($input{'type'}) && !ref($input{'type'})) {
			# our %input = ( type => 'string' );
			my $type = $input{'type'};

			if($input{'enum'}) {
				if($input{'memberof'}) {
					die 'input has both enum and memberof';
				}
				$input{'memberof'} = delete $input{'enum'};
			}
			foreach my $field(keys %input) {
				next if($field =~ /^_/);	# Ignore comments
				if(!grep({ $_ eq $field } ('type', 'min', 'max', 'optional', 'matches', 'can', 'memberof', 'position'))) {
					die("TODO: handle schema keyword '$field'");
				}
			}

			if ($type eq 'string') {
				push @cases, @{_generate_string_cases('_input', \%input, \%mandatory_args, _LINE => __LINE__)};
				# push @cases, { '_input' => "emoji \x{1F600}" };
			} else {
				die "TODO: type $type";
			}
		} else {
			# our %input = ( str => { type => 'string' } );

			foreach my $arg_name (keys %input) {
				my $spec = $input{$arg_name} || {};
				my $type = lc((!ref($spec)) ? $spec : $spec->{type}) || 'string';

				if(ref($spec) && $spec->{'enum'}) {
					if($spec->{'memberof'}) {
						die "$arg_name has both enum and memberof";
					}
					$spec->{'memberof'} = delete $spec->{'enum'};
				}
				foreach my $field(keys %{$spec}) {
					next if($field =~ /^_/);	# Ignore comments
					if(!grep({ $_ eq $field } ('type', 'min', 'max', 'optional', 'matches', 'can', 'memberof', 'position', 'isa'))) {
						diag(__LINE__, ": TODO: handle schema keyword '$field'");
					}
				}

				# --- Type-based seeds ---
				if(($type eq 'number') || ($type eq 'float')) {
					push @cases, @{_generate_float_cases($arg_name, $spec, \%mandatory_args, _LINE => __LINE__)};
				} elsif ($type eq 'integer') {
					# Probably duplicated below, but here as well just in case
					push @cases, @{_generate_integer_cases($arg_name, $spec, \%mandatory_args, _LINE => __LINE__)};
				} elsif ($type eq 'string') {
					push @cases, @{_generate_string_cases($arg_name, $spec, \%mandatory_args, _LINE => __LINE__)};
				} elsif ($type eq 'boolean') {
					push @cases, @{_generate_boolean_cases($arg_name, $spec, \%mandatory_args, _LINE => __LINE__)};
				} elsif ($type eq 'hashref') {
					push @cases,
						{ %mandatory_args, $arg_name => { a => 1 } },
						{ %mandatory_args, $arg_name => [], _STATUS => 'DIES' },
						{ %mandatory_args, $arg_name => 66, _STATUS => 'DIES' },
						{ %mandatory_args, $arg_name => sub { die 'fail' }, _STATUS => 'DIES' },
						{ %mandatory_args, $arg_name => 'scalar when hashref is needed', _STATUS => 'DIES' },
						{ %mandatory_args, $arg_name => \'scalarref when hashref is needed', _STATUS => 'DIES' };
					if($config{test_undef}) {
						push @cases,
							{ %mandatory_args, $arg_name => { nested => { 'first', undef, 'third' } }, _DESCRIPTION => 'nested hash with undef' } if($config{test_undef});
					}
				} elsif ($type eq 'arrayref') {
					my $circular_ref = [];
					push @{$circular_ref}, $circular_ref;

					push @cases,
						{ %mandatory_args, $arg_name => [1,2] },
						{ %mandatory_args, $arg_name => $circular_ref, _STATUS => 'DIES', _DESCRIPTION => 'circular ref is caught' },
						{ %mandatory_args, $arg_name => { a => 1 }, _STATUS => 'DIES' };
				} elsif($type eq 'object') {
					if($spec->{'isa'}) {
						push @cases, { $arg_name => { a => 1 }, _STATUS => 'DIES', _LINE => __LINE__ };
						push @cases, { $arg_name => [], _STATUS => 'DIES', _LINE => __LINE__ };

						# Test dies when given the wrong type of object
						push @cases, { $arg_name => new_ok('MyTestPackage'), _STATUS => 'DIES', _LINE => __LINE__ };

						use_ok($spec->{isa});
						push @cases, { $arg_name => new_ok($spec->{isa}) };
					} elsif(!$spec->{can}) {
						Carp::carp("neither 'isa' nor 'can' is defined - what type of object should be sent?");
					}
				} elsif($type eq 'any') {
					push @cases,
						{ %mandatory_args, $arg_name => 'string_value' },
						{ %mandatory_args, $arg_name => 42 },
						{ %mandatory_args, $arg_name => 3.14 },
						{ %mandatory_args, $arg_name => [] },
						{ %mandatory_args, $arg_name => {} };
					if($config{test_undef}) {
						push @cases, { %mandatory_args, $arg_name => undef };
					}
				}

				# --- matches (regex) ---
				if (defined $spec->{matches}) {
					my $regex = $spec->{matches};
					for my $string(@regex_tests) {
						if($string =~ $regex) {
							push @cases, { %mandatory_args, ( $arg_name => $string ) };
						} else {
							push @cases, { %mandatory_args, ( $arg_name => $string, _STATUS => 'DIES' ) };
						}
					}
				}

				# --- nomatch (regex) ---
				if (defined $spec->{nomatch}) {
					my $regex = $spec->{nomatch};
					for my $string(@regex_tests) {
						if($string =~ $regex) {
							push @cases, { %mandatory_args, ( $arg_name => $string, _STATUS => 'DIES' ) };
						} else {
							push @cases, { %mandatory_args, ( $arg_name => $string ) };
						}
					}
				}

				# --- memberof ---
				if (defined $spec->{memberof}) {
					my @set = @{ $spec->{memberof} };
					push @cases, { %mandatory_args, ( $arg_name => $set[0] ) } if @set;
					push @cases, { %mandatory_args, ( $arg_name => '_not_in_set_', _STATUS => 'DIES' ) };
				}

				# --- notmemberof ---
				if (defined $spec->{notmemberof}) {
					my @set = @{ $spec->{notmemberof} };
					push @cases, { %mandatory_args, ( $arg_name => $set[0], _STATUS => 'DIES' ) } if @set;
					push @cases, { %mandatory_args, ( $arg_name => '_not_in_set_' ) };
				}

				# --- semantic ---
				if(defined(my $semantic = $spec->{'semantic'})) {
					if(defined(my $semantic = $spec->{'semantic'})) {
						push @cases, { %mandatory_args, ( -1, _STATUS => 'DIES' ) },
							{ %mandatory_args, ( $arg_name => 0 ) },
							{ %mandatory_args, ( $arg_name => 1 ) },
							{ %mandatory_args, ( $arg_name => time ) },
							{ %mandatory_args, ( $arg_name => 2147483647 ) },
							{ %mandatory_args, ( 45.67, _STATUS => 'DIES', _DESCRIPTION => 'UNIX timestamp should not be a float' ) },
							{ %mandatory_args, ( $arg_name => 2147483648, _STATUS => 'DIES' ) };
					} elsif($semantic eq 'email') {
						push @cases,
							{ %mandatory_args, ( $arg_name => 'user@example.com' ) },
							{ %mandatory_args, ( $arg_name => 'user+tag@sub.example.co.uk' ) },
							{ %mandatory_args, ( $arg_name => '@nodomain', _STATUS => 'DIES' ) },
							{ %mandatory_args, ( $arg_name => 'noatsign', _STATUS => 'DIES' ) },
							{ %mandatory_args, ( $arg_name => 'user@', _STATUS => 'DIES' ) },
							{ %mandatory_args, ( $arg_name => '', _STATUS => 'DIES' ) },
							{ %mandatory_args, ( $arg_name => 'user@' . ('a' x 256) . '.com', _STATUS => 'DIES' ) };
					} elsif($semantic eq 'filepath') {
						push @cases,
							{ %mandatory_args, ( $arg_name => '/tmp/test.txt' ) },
							{ %mandatory_args, ( $arg_name => 'relative/path.txt' ) },
							{ %mandatory_args, ( $arg_name => '.' ) },
							{ %mandatory_args, ( $arg_name => '..' ) },
							{ %mandatory_args, ( $arg_name => '../../etc/passwd', _STATUS => 'DIES' ) },
							{ %mandatory_args, ( $arg_name => '/etc/passwd' . "\0", _STATUS => 'DIES' ) },
							{ %mandatory_args, ( $arg_name => '', _STATUS => 'DIES' ) },
							{ %mandatory_args, ( $arg_name => '/' . ('a' x 4096), _STATUS => 'DIES' ) };
						if($^O eq 'MSWin32') {
							push @cases,
								{ %mandatory_args, ( $arg_name => 'C:\\Users\\test\\file.txt' ) },
								{ %mandatory_args, ( $arg_name => 'D:\\' ) },
								{ %mandatory_args, ( $arg_name => 'relative\\path.txt' ) },
								{ %mandatory_args, ( $arg_name => 'C:\\..\\..\\Windows\\System32', _STATUS => 'DIES' ) },
								{ %mandatory_args, ( $arg_name => 'C:\\path\\' . "\0" . 'file', _STATUS => 'DIES' ) },
								{ %mandatory_args, ( $arg_name => 'COM1', _STATUS => 'DIES' ) },   # reserved device name
								{ %mandatory_args, ( $arg_name => 'NUL', _STATUS => 'DIES' ) },    # reserved device name
								{ %mandatory_args, ( $arg_name => 'C:\\path\\file?.txt', _STATUS => 'DIES' ) },   # wildcard
								{ %mandatory_args, ( $arg_name => 'C:\\path\\file*.txt', _STATUS => 'DIES' ) },   # wildcard
								{ %mandatory_args, ( $arg_name => '\\\\server\\share\\file.txt' ) };  # UNC path
						}
					} elsif($semantic eq 'date_string') {
						push @cases,
							{ %mandatory_args, ( $arg_name => '2024-01-01' ) },
							{ %mandatory_args, ( $arg_name => '1970-01-01' ) },
							{ %mandatory_args, ( $arg_name => '2024-02-29' ) },   # leap day
							{ %mandatory_args, ( $arg_name => '2023-02-29', _STATUS => 'DIES' ) },  # not a leap year
							{ %mandatory_args, ( $arg_name => '2024-13-01', _STATUS => 'DIES' ) },  # month 13
							{ %mandatory_args, ( $arg_name => '2024-00-01', _STATUS => 'DIES' ) },  # month 0
							{ %mandatory_args, ( $arg_name => '01-01-2024', _STATUS => 'DIES' ) },  # wrong order
							{ %mandatory_args, ( $arg_name => '2024/01/01', _STATUS => 'DIES' ) },  # wrong separator
							{ %mandatory_args, ( $arg_name => 'not-a-date', _STATUS => 'DIES' ) },
							{ %mandatory_args, ( $arg_name => '', _STATUS => 'DIES' ) };
					} elsif($semantic eq 'iso8601_string') {
						push @cases,
							{ %mandatory_args, ( $arg_name => '2024-01-01T00:00:00Z' ) },
							{ %mandatory_args, ( $arg_name => '2024-06-15T12:30:45Z' ) },
							{ %mandatory_args, ( $arg_name => '2024-01-01T00:00:00+05:30' ) },
							{ %mandatory_args, ( $arg_name => '2024-01-01' ) },          # date only - check if accepted
							{ %mandatory_args, ( $arg_name => '2024-01-01T25:00:00Z', _STATUS => 'DIES' ) },  # hour 25
							{ %mandatory_args, ( $arg_name => '2024-01-01T00:61:00Z', _STATUS => 'DIES' ) },  # minute 61
							{ %mandatory_args, ( $arg_name => 'not-a-datetime', _STATUS => 'DIES' ) },
							{ %mandatory_args, ( $arg_name => '', _STATUS => 'DIES' ) };
					} else {
						diag("semantic type $semantic is not yet supported");
					}
				}
			}
		}
	}

	# Optional deduplication
	# my %seen;
	# @cases = grep { !$seen{join '|', %$_}++ } @cases;

	# Random data test cases
	# Uses type_edge_cases sometimes
	if(scalar keys %input) {
		if(((scalar keys %input) == 1) && exists($input{'type'}) && !ref($input{'type'})) {
			# our %input = ( type => 'string' );
			my $type = $input{'type'};
			for (1..[% iterations_code %]) {
				my $case_input;
				if (@edge_case_array && rand() < PROB_EDGE_CASE) {
					# Sometimes pick a field-specific edge-case
					$case_input = _pick_from(\@edge_case_array);
				} elsif(exists $type_edge_cases{$type} && rand() < 0.3) {
					# Sometimes pick a type-level edge-case
					$case_input = _pick_from($type_edge_cases{$type});
				} elsif($type eq 'string') {
					if($input{matches}) {
						$case_input = Data::Random::String::Matches->create_random_string({ regex => $input{'matches'} });
					} else {
						$case_input = rand_str();
					}
				} elsif($type eq 'integer') {
					my $min = $input{'min'} // 0;

					$case_input = int(rand_int() + $min);
					# If it's takes an integer, a float should die
					push @cases, { _input => $case_input + 0.1, _STATUS => 'DIES', _LINE => __LINE__ };
				} elsif(($type eq 'number') || ($type eq 'float')) {
					$case_input = abs(rand_num()) + $input{'min'};
				} elsif($type eq 'boolean') {
					$case_input = rand_bool();
				} else {
					die "TODO: type $type";
				}
				push @cases, { _input => $case_input, _STATUS => 'OK', _LINE => __LINE__ } if($case_input);
			}
		} else {
			# our %input = ( str => { type => 'string' } );
			push @cases, @{generate_tests(\%input, \%mandatory_args, _LINE => __LINE__)};

		}
	}

	# edge-cases
	if($config{'test_undef'}) {
		if($all_optional) {
			push @cases, { '_DESCRIPTION' => 'No args since all are optional' };
		} else {
			# Note that this is set on the input rather than output
			push @cases, { '_STATUS' => 'DIES' };	# At least one argument is needed
		}
	}

	if(scalar keys %input) {
		push @cases, { '_STATUS' => 'DIES', map { $_ => undef } keys %input } if($config{'test_undef'});
	} else {
		push @cases, { '_DESCRIPTION' => 'Takes no input' };	# Takes no input
	}

	# If it's not in mandatory_strings it sets to 'undef' which is the idea, to test { value => undef } in the args
	# _LINE has to go first or else the undef in there mucks up the hash format
	push @cases, { _LINE => __LINE__, map { $_ => $mandatory_strings{$_} } keys %input, %mandatory_objects } if($config{'test_undef'} && !$positions);

	push @candidate_bad, '' if($config{'test_empty'});
	push @candidate_bad, undef if($config{'test_undef'});
	push @candidate_bad, "\0" if($config{'test_nuls'});

	# generate numeric, string, hashref and arrayref min/max edge cases
	# TODO: For hashref and arrayref, if there's a $spec->{schema} field, use that for the data that's being generated
	if(((scalar keys %input) == 1) && exists($input{'type'}) && !ref($input{'type'})) {
		# our %input = ( type => 'string' );
		my $type = $input{type};
		if (exists $input{memberof} && ref $input{memberof} eq 'ARRAY' && @{$input{memberof}}) {
			# Generate edge cases for memberof inside values
			foreach my $val (@{$input{memberof}}) {
				push @cases,
					{ _input => $val, _LINE => __LINE__ },
					{ _input => " $val", _LINE => __LINE__, _STATUS => 'DIES' },
					{ _input => "$val ", _LINE => __LINE__, _STATUS => 'DIES' },
					{ _input => substr($val, 0, -1), _LINE => __LINE__, _STATUS => 'DIES' };
				if($val =~ /[A-Z]/) {
					push @cases, { _input => lc($val), _LINE => __LINE__, _STATUS => 'DIES' };
				}
				if($val =~ /[a-z]/) {
					push @cases, { _input => uc($val), _LINE => __LINE__, _STATUS => 'DIES' };
				}
			}
			# outside value
			my $outside;
			if(($type eq 'integer') || ($type eq 'number') || ($type eq 'float')) {
				$outside = (sort { $a <=> $b } @{$input{memberof}})[-1] + 1;
			} else {
				$outside = 'INVALID_MEMBEROF';
			}
			push @cases, { _input => $outside, _STATUS => 'DIES' };
		} else {
			# Generate edge cases for min/max
			if($type eq 'integer') {
				push @cases, @{_generate_integer_cases('_input', \%input, \%mandatory_args, _LINE => __LINE__)};
			} elsif(($type eq 'number') || ($type eq 'float')) {
				push @cases, @{_generate_float_cases('_input', \%input, \%mandatory_args, _LINE => __LINE__)};
			} elsif ($type eq 'string') {
				push @cases, @{_generate_string_cases('_input', \%input, \%mandatory_args, _LINE => __LINE__)};
			} elsif ($type eq 'arrayref') {
				if (defined $input{min}) {
					my $len = $input{min};
					push @cases, { _input => [ (1) x ($len + 1) ] };	# just inside
					push @cases, { _input => [ (1) x $len ] };	# border
					push @cases, { _input => [ (1) x ($len - 1) ], _STATUS => 'DIES' } if $len > 0; # outside
				} else {
					push @cases, { _input => [] } if($config{'test_empty'});	# No min, empty array should be allowable
				}
				if (defined $input{max}) {
					my $len = $input{max};
					push @cases, { _input => [ (1) x ($len - 1) ] };	# just inside
					push @cases, { _input => [ (1) x $len ] };	# border
					push @cases, { _input => [ (1) x ($len + 1) ], _STATUS => 'DIES' }; # outside
				}
			} elsif ($type eq 'hashref') {
				if (defined $input{min}) {
					my $len = $input{min};
					push @cases, { _input => { map { "k$_" => 1 }, 1 .. ($len + 1) } };
					push @cases, { _input => { map { "k$_" => 1 }, 1 .. $len } };
					push @cases, { _input => { map { "k$_" => 1 }, 1 .. ($len - 1) }, _STATUS => 'DIES' } if $len > 0;
				} else {
					push @cases, { _input => {} } if($config{'test_empty'});	# No min, empty hash should be allowable
				}
				if (defined $input{max}) {
					my $len = $input{max};
					push @cases, { _input => { map { "k$_" => 1 }, 1 .. ($len - 1) } };
					push @cases, { _input => { map { "k$_" => 1 }, 1 .. $len } };
					push @cases, { _input => { map { "k$_" => 1 }, 1 .. ($len + 1) }, _STATUS => 'DIES' };
				}
			} elsif ($type eq 'boolean') {
				push @cases, @{_generate_boolean_cases('_input', \%input, \%mandatory_args)};
			}

			# Test all edge cases
			foreach my $edge(@edge_case_array) {
				push @cases, { _input => $edge, _DESCRIPTION => 'edge case' };
			}
		}
	} else {
		# our %input = ( str => { type => 'string' } );
		push @cases, @{generate_tests(\%input, \%mandatory_args)};
	}

	# fuzzing can easily generate repeats
	if($config{'dedup'}) {
		return _dedup_cases(\@cases);
	}

	# use Data::Dumper;
	# die(Dumper(@cases));

	return \@cases;
}

# Functions to generate test cases
sub _generate_integer_cases {
	my ($arg_name, $spec, $mandatory_args) = @_;
	my @cases;

	foreach my $i (-42, -1, 42) {
		if(((!defined $spec->{min}) || ($spec->{min} <= $i)) && ((!defined($spec->{max})) || ($spec->{max} >= $i))) {
			push @cases, { %{$mandatory_args}, ( $arg_name => $i ) };
		} else {
			push @cases, { %{$mandatory_args}, ( $arg_name => $i, _STATUS => 'DIES' ) };
		}
	}

	[% IF module %]
		# Send wrong data type - builtins aren't good at checking this
		push @cases,
			{ %{$mandatory_args}, ( $arg_name => "test string in integer field $arg_name", _STATUS => 'DIES', _LINE => __LINE__ ) },
			{ %{$mandatory_args}, ( $arg_name => {}, _STATUS => 'DIES', _LINE => __LINE__ ) },
			{ %{$mandatory_args}, ( $arg_name => 3.14, _STATUS => 'DIES' ) },	# Float
			{ %{$mandatory_args}, ( $arg_name => 'xyz', _STATUS => 'DIES' ) },
			{ %{$mandatory_args}, ( $arg_name => \42, _STATUS => 'DIES' ) },	# Scalar ref
			{ %{$mandatory_args}, ( $arg_name => *STDOUT, _STATUS => 'DIES' ) },	# Global variable
			{ %{$mandatory_args}, ( $arg_name => [], _STATUS => 'DIES', _LINE => __LINE__ ) };
	[% END %]

	# min/max numeric boundaries
	if (defined $spec->{min}) {
		my $min = $spec->{min};
		push @cases,
			{ %{$mandatory_args}, ( $arg_name => $min - 1, _STATUS => 'DIES' ) },
			{ %{$mandatory_args}, ( $arg_name => "  $min  ", _LINE => __LINE__ ) },	# border, padded integer
			{ %{$mandatory_args}, ( $arg_name => $min, _LINE => __LINE__ ) },	# border
			{ %{$mandatory_args}, ( $arg_name => $min + 1 ) };	# just inside

		if(!defined $spec->{max}) {
			push @cases, { %{$mandatory_args}, ( $arg_name => int($min + abs(rand_int())) ), _LINE => __LINE__ };
		}
		if($min <= 0) {
			push @cases, { %{$mandatory_args}, ( $arg_name => -0.0, _STATUS => 'OK' ) };	# Negative 0
		}
	} else {
		push @cases,
			{ %{$mandatory_args}, ( $arg_name => 1e5, _STATUS => 'OK' ) },	# Scientific notation
			{ %{$mandatory_args}, ( $arg_name => 1_000, _STATUS => 'OK' ) },	# Underscored
			{ %{$mandatory_args}, ( $arg_name => '   42   ', _STATUS => 'OK' ) };	# Padded integer
	}

	if (defined $spec->{max}) {
		my $max = $spec->{max};
		push @cases,
			{ %{$mandatory_args}, ( $arg_name => $max - 1 ) },
			{ %{$mandatory_args}, ( $arg_name => $max ) },
			{ %{$mandatory_args}, ( $arg_name => $max + 1, _STATUS => 'DIES' ) };

		if(defined $spec->{min}) {
			# Test 0 if it's in range
			push @cases, { %{$mandatory_args}, ( $arg_name => 0 ) } if($spec->{'min'} >= 0);
		} else {
			push @cases, { %{$mandatory_args}, ( $arg_name => $max - rand_int() ), _DESCRIPTION => 'max is defined but min is not' };
			if($max >= 0) {
				push @cases, { %{$mandatory_args}, ( $arg_name => -0.0, _STATUS => 'OK' ) };	# Negative 0
				if($max == 0) {
					push @cases, { %{$mandatory_args}, ( $arg_name => abs(rand_int()) * -1 ), _LINE => __LINE__ };	# Any negative integer
				}
			}
		}
	} elsif(!defined $spec->{min}) {
		# Can take any number, so give it one
		push @cases,
			{ %{$mandatory_args}, ( $arg_name => rand_int() ), _LINE => __LINE__ },
			{ %{$mandatory_args}, ( $arg_name => 0) };	# 0 is in range
	}

	return \@cases;
}

sub _generate_float_cases {
	my ($arg_name, $spec, $mandatory_args) = @_;
	my @cases;

	if((!defined $spec->{min}) || ($spec->{min} <= -0.1)) {
		push @cases, { %{$mandatory_args}, ( $arg_name => -0.1, _LINE => __LINE__ ) };
	}
	if((!defined $spec->{min}) || ($spec->{min} <= 43.56)) {
		push @cases, { %{$mandatory_args}, ( $arg_name => 43.56 ) };
	}

	[% IF module %]
		# Send wrong data type - builtins aren't good at checking this
		push @cases,
			{ %{$mandatory_args}, ( $arg_name => "test string in float field $arg_name", _STATUS => 'DIES', _LINE => __LINE__ ) },
			{ %{$mandatory_args}, ( $arg_name => {}, _STATUS => 'DIES', _LINE => __LINE__ ) },
			{ %{$mandatory_args}, ( $arg_name => \42.1, _STATUS => 'DIES' ) },	# Scalar ref
			# NaN and Inf are valid according to looks_like_number() so we
			# cannot assume they die
			# { %{$mandatory_args}, ( $arg_name => "NaN", _STATUS => 'DIES' ) },
			{ %{$mandatory_args}, ( $arg_name => [], _STATUS => 'DIES', _LINE => __LINE__ ) };
	[% END %]

	# min/max numeric boundaries
	if (defined $spec->{min}) {
		my $min = $spec->{min};
		push @cases,
			{ %{$mandatory_args}, ( $arg_name => $min - 0.001, _STATUS => 'DIES' ), _DESCRIPTION => 'float below minimum value is denied' },
			{ %{$mandatory_args}, ( $arg_name => $min, _LINE => __LINE__ ), _DESCRIPTION => 'float minimum value is allowed' },	# border
			{ %{$mandatory_args}, ( $arg_name => $min + 0.001 ), _DESCRIPTION => 'float above minimum value is allowed' };	# just inside

		[% IF module %]
			push @cases, { %{$mandatory_args}, ( $arg_name => "-inf", _STATUS => 'DIES' ) };
		[% END %]

		if(!defined $spec->{max}) {
			if($min <= 0) {
				push @cases, { %{$mandatory_args}, ( $arg_name => $min + abs(rand_num()) ), _LINE => __LINE__ };	# Any positive number
			} else {
				push @cases, { %{$mandatory_args}, ( $arg_name => $min + rand_num() ), _DESCRIPTION => 'min is defined but max is not' };
			}
		}
	}
	if (defined $spec->{max}) {
		my $max = $spec->{max};
		push @cases,
			{ %{$mandatory_args}, ( $arg_name => $max - 0.000001 ) },
			{ %{$mandatory_args}, ( $arg_name => $max ) },
			{ %{$mandatory_args}, ( $arg_name => $max + 0.000001, _STATUS => 'DIES' ) };

		[% IF module %]
			push @cases,
				{ %{$mandatory_args}, ( $arg_name => "inf", _STATUS => 'DIES' ) },
				{ %{$mandatory_args}, ( $arg_name => 9**9**9, _STATUS => 'DIES' ) };
		[% END %]

		if(defined $spec->{min}) {
			# Test 0 if it's in range
			push @cases, { %{$mandatory_args}, ( $arg_name => 0 ) } if($spec->{'min'} >= 0);
		} else {
			push @cases, { %{$mandatory_args}, ( $arg_name => $max - rand_num() ), _LINE => __LINE__ };
			if($max == 0) {
				push @cases, { %{$mandatory_args}, ( $arg_name => abs(rand_num()) * -0.00000001 ) };	# Any negative number
			}
		}
	} elsif(!defined $spec->{min}) {
		# Can take any number, so give it some
		push @cases,
			{ %{$mandatory_args}, ( $arg_name => rand_num(), _LINE => __LINE__ ) },
			{ %{$mandatory_args}, ( $arg_name => 1.23 ) },
			{ %{$mandatory_args}, ( $arg_name => -42.1 ) },
			{ %{$mandatory_args}, ( $arg_name => -0.0 ) },	# -0 is in range
			{ %{$mandatory_args}, ( $arg_name => 0 ) };	# 0 is in range
	}

	return \@cases;
}

# basic boolean edge cases
sub _generate_boolean_cases {
	my ($arg_name, $spec, $mandatory_args) = @_;

	my @cases;

	if(exists($spec->{'memberof'}) && (ref($spec->{'memberof'} eq 'ARRAY'))) {
		# memberof already defines allowed booleans
		foreach my $val (@{$spec->{memberof}}) {
			push @cases, { %{$mandatory_args}, ( $arg_name => $val ) };
		}
	} else {
		@cases = (
			{ %{$mandatory_args}, ( $arg_name => 0 ) },
			{ %{$mandatory_args}, ( $arg_name => 1 ) },
			{ %{$mandatory_args}, ( $arg_name => 'true' ) },
			{ %{$mandatory_args}, ( $arg_name => 'false' ) },
			{ %{$mandatory_args}, ( $arg_name => 'off' ) },
			{ %{$mandatory_args}, ( $arg_name => 'on' ) },
			{ %{$mandatory_args}, ( $arg_name => 'yes' ) },
			{ %{$mandatory_args}, ( $arg_name => 'no' ) },
			{ %{$mandatory_args}, ( $arg_name => 'xyzzy', _STATUS => 'DIES' ) },	# invalid boolean
			{ %{$mandatory_args}, ( $arg_name => -1, _STATUS => 'DIES' ) },	# invalid boolean
			{ %{$mandatory_args}, ( $arg_name => 2, _STATUS => 'DIES' ) },	# invalid boolean
			{ %{$mandatory_args}, ( $arg_name => bless({}, 'Evil::Class'), _STATUS => 'DIES', _LINE => __LINE__ ) },
			{ %{$mandatory_args}, ( $arg_name => [ 1 ], _STATUS => 'DIES' ) }	# invalid boolean
		);
	}

	push @cases, { %{$mandatory_args}, ( $arg_name => undef, _STATUS => 'DIES' ) } if($config{'test_undef'});
	push @cases, { %{$mandatory_args}, ( $arg_name => "\0", _STATUS => 'DIES' ) } if($config{'test_nuls'});

	return \@cases;
}

sub _generate_string_cases
{
	my ($arg_name, $spec, $mandatory_args, $properties_array_ref) = @_;

	my @cases;

	if (defined $spec->{min}) {
		my $len = $spec->{min};
		if(my $re = $spec->{matches}) {
			if(ref($re) ne 'Regexp') {
				$re = qr/$re/;
			}
			my $random_string;
			if($spec->{'max'}) {
				$random_string = Data::Random::String::Matches->create_random_string({ length => $spec->{'max'}, regex => $re });
			} elsif($spec->{'min'}) {
				$random_string = Data::Random::String::Matches->create_random_string({ length => $spec->{'min'}, regex => $re });
			} else {
				$random_string = Data::Random::String::Matches->create_random_string({ regex => $re });
			}
			# Is hello allowed?
			foreach my $str('hello', $random_string) {
				if($str =~ $re) {
					if(!defined($spec->{'memberof'}) || (grep { $_ eq $str } @{$spec->{'memberof'}})) {
						if(defined($spec->{'notmemberof'}) && (grep { $_ eq $str } @{$spec->{'notmemberof'}})) {
							push @cases, { %{$mandatory_args}, ( $arg_name => $str, _STATUS => 'DIES' ) };
						} else {
							push @cases, { %{$mandatory_args}, ( $arg_name => $str ) };
						}
					} elsif(defined($spec->{'memberof'}) && !defined($spec->{'max'})) {
						# Data::Random
						push @cases, { %{$mandatory_args}, ( _input => (rand_set(set => $spec->{'memberof'}, size => 1))[0] ) }
					} else {
						push @cases, { %{$mandatory_args}, ( $arg_name => $str, _STATUS => 'DIES' ) };
					}
				} else {
					push @cases, { %{$mandatory_args}, ( $arg_name => $str, _STATUS => 'DIES' ) };
				}
			}
			for my $count ($len + 1, $len, $len - 1) {
				next if ($count < 0);
				my $str = rand_str($count);
				if($str =~ $re) {
					push @cases, { %{$mandatory_args}, ( $arg_name => $str ) };
				} else {
					push @cases, { %{$mandatory_args}, ( $arg_name => $str, _STATUS => 'DIES' ) };
				}
			}
		} else {
			# matches is not defined
			if(!defined($spec->{'memberof'}) || (grep { $_ eq 'hello' } @{$spec->{'memberof'}})) {
				if(defined($spec->{'notmemberof'}) && (grep { $_ eq 'hello' } @{$spec->{'notmemberof'}})) {
					push @cases, { %{$mandatory_args}, ( $arg_name => 'hello', _LINE => __LINE__, _STATUS => 'DIES' ) };
				} elsif((!defined($spec->{max})) || ($spec->{max} >= 5)) {
					push @cases, { %{$mandatory_args}, ( $arg_name => 'hello', _LINE => __LINE__, _STATUS => 'OK' ) };
				} else {
					push @cases,
						{ %{$mandatory_args}, ( $arg_name => 'plugh', _LINE => __LINE__, _STATUS => 'DIES' ) },
						{ %{$mandatory_args}, ( $arg_name => '     ', _LINE => __LINE__, _STATUS => 'DIES' ) };
				}
				# Check that trimmed strings work
				push @cases, { %{$mandatory_args}, ( $arg_name => ' ' x $len, _LINE => __LINE__, _STATUS => 'OK' ) };
			} else {
				push @cases, { %{$mandatory_args}, ( $arg_name => 'hello', _LINE => __LINE__, _STATUS => 'DIES' ) };
			}
			if(!defined($spec->{'memberof'})) {
				push @cases, { %{$mandatory_args}, ( $arg_name => "he\nlo", _LINE => __LINE__, _STATUS => 'OK' ) },
					{ %{$mandatory_args}, ( $arg_name => "\nhell", _LINE => __LINE__, _STATUS => 'OK' ) },
					{ %{$mandatory_args}, ( $arg_name => "hell\n", _LINE => __LINE__, _STATUS => 'OK' ) },
					{ %{$mandatory_args}, ( $arg_name => ' hell', _LINE => __LINE__, _STATUS => 'OK' ) },
					{ %{$mandatory_args}, ( $arg_name => rand_str($len) . "\n" . rand_str($len), _LINE => __LINE__, _STATUS => 'OK' ) };
			}

			if($len <= 0) {
				push @cases, { %{$mandatory_args}, ( $arg_name => '', _LINE => __LINE__, _DESCRIPTION => 'min == 0 so empty should be allowed' ) } if($config{'test_empty'});	# min == 0, empty string should be allowable
				push @cases,
					{ %{$mandatory_args}, ( $arg_name => ' ', _LINE => __LINE__ ) },
					{ %{$mandatory_args}, ( $arg_name => "\n", _LINE => __LINE__ ) },
					# Don't confuse if() with if(defined())
					{ %{$mandatory_args}, ( $arg_name => '0', _STATUS => 'DIES' ) };

				if($config{'test_security'}) {
					push @cases,
						{ %{$mandatory_args}, ( $arg_name => '<script>alert(1)</script>', _LINE => __LINE__ ) },
						{ %{$mandatory_args}, ( $arg_name => "'; DROP TABLE foo --", _LINE => __LINE__ ) };
				}
			}
		}
	} elsif($config{'test_empty'}) {
		# No min, empty string should be allowable
		if(defined($spec->{'memberof'}) && (!grep { $_ eq '' } @{$spec->{'memberof'}})) {
			push @cases, { %{$mandatory_args}, ( $arg_name => '', _LINE => __LINE__, _STATUS => 'DIES' ) };
		} else {
			push @cases, { %{$mandatory_args}, ( $arg_name => '', _LINE => __LINE__ ) };
		}
	}
	if (defined $spec->{max}) {
		my $len = $spec->{max};
		if((!defined($spec->{min})) || ($spec->{min} != $len)) {
			if(my $re = $spec->{matches}) {
				for my $count ($len - 1, $len, $len + 1) {
					my $str = rand_str($count);
					if($str =~ $re) {
						if($count > $len) {
							push @cases, { %{$mandatory_args}, ( $arg_name => $str, _LINE => __LINE__, _STATUS => 'DIES' ) };
						} else {
							push @cases, { %{$mandatory_args}, ( $arg_name => $str, _LINE => __LINE__ ) };
						}
					} else {
						push @cases, { %{$mandatory_args}, ( $arg_name => $str, _STATUS => 'DIES', _LINE => __LINE__ ) };
					}
				}
			} else {
				push @cases, { %{$mandatory_args}, ( $arg_name => rand_str($len - 1), _LINE => __LINE__ ) };	# just inside
				push @cases, { %{$mandatory_args}, ( $arg_name => rand_str($len), _LINE => __LINE__ ) };	# border
				push @cases, { %{$mandatory_args}, ( $arg_name => rand_str($len + 1), _LINE => __LINE__, _STATUS => 'DIES' ) }; # outside
			}
		} elsif(defined($spec->{min}) && ($spec->{min} == $len)) {
			if($len >= 2) {
				push @cases, { %{$mandatory_args}, ( $arg_name => ' ' x $len - 1, _LINE => __LINE__, _DESCRIPTION => 'max == min', _STATUS => 'DIES' ) };
			}
			push @cases, { %{$mandatory_args}, ( $arg_name => ' ' x $len, _LINE => __LINE__, _DESCRIPTION => 'max == min' ) };
			push @cases, { %{$mandatory_args}, ( $arg_name => ' ' x $len + 1, _LINE => __LINE__, _DESCRIPTION => 'max == min', _STATUS => 'DIES' ) };
		}
	} elsif((!$spec->{matches}) && (!$spec->{memberof})) {
		# TODO: send them if they match the regex
		if(exists($spec->{'min'})) {
			push @cases, { %{$mandatory_args}, ( $arg_name => rand_str(($spec->{'min'} + 1) * 1_000), _LINE => __LINE__ ) };
		} else {
			push @cases, { %{$mandatory_args}, ( $arg_name => rand_str(65535), _LINE => __LINE__, _DESCRIPTION => 'Long string nearly 64K characters' ) };
			push @cases, { %{$mandatory_args}, ( $arg_name => rand_str(65536), _LINE => __LINE__, _DESCRIPTION => 'Long string 64K characters' ) };
		}
		if((!exists($spec->{min})) || ($spec->{min} <= 1)) {
			push @cases, { %{$mandatory_args}, ( $arg_name => "\x{FEFF}", _STATUS => 'OK', _LINE => __LINE__, _DESCRIPTION => 'Byte order marker' ) };
		} else {
			push @cases, { %{$mandatory_args}, ( $arg_name => "\x{FEFF}", _STATUS => 'DIES', _LINE => __LINE__, _DESCRIPTION => 'Byte order marker' ) };
		}
	}

	if((!exists($spec->{min})) || ($spec->{min} == 0)) {
		# '' should die unless it's in the memberof list
		if(defined($spec->{'memberof'}) && (!grep { $_ eq '' } @{$spec->{'memberof'}})) {
			push @cases, { %{$mandatory_args}, ( $arg_name => '', _NAME => $arg_name, _STATUS => 'DIES', _LINE => __LINE__ ) }
		} elsif(defined($spec->{'memberof'}) && !defined($spec->{'max'})) {
			# Data::Random
			push @cases, { %{$mandatory_args}, _input => (rand_set(set => $spec->{'memberof'}, size => 1))[0] }
		} elsif($config{'test_empty'} && !$spec->{'memberof'}) {
			push @cases, { %{$mandatory_args}, ( $arg_name => '', _NAME => $arg_name, _LINE => __LINE__ ) };
		}
	}
	# push @cases, { $arg_name => "emoji \x{1F600}" };

	unless(defined($spec->{memberof}) || defined($spec->{matches})) {
		# --- min/max string/array boundaries ---
		if(defined $spec->{min}) {
			my $len = $spec->{min};
			if($len == 1) {
				if($config{'test_empty'}) {
					push @cases, { %{$mandatory_args}, ( $arg_name => '', _STATUS => 'DIES', _LINE => __LINE__ ) };
				}
			} elsif($len > 0) {
				push @cases, { %{$mandatory_args}, ( $arg_name => rand_str($len - 1), _STATUS => 'DIES' ) };
			}
			if((!defined($spec->{max})) || ($spec->{max} >= $len + 1)) {
				push @cases,
					{ %{$mandatory_args}, ( $arg_name => rand_str($len) ) },
					{ %{$mandatory_args}, ( $arg_name => rand_str($len + 1) ) };
			}
			if($len <= 1) {
				push @cases,
					{ %{$mandatory_args}, ( $arg_name => "\n" ) },	# new lines are fun
					{ %{$mandatory_args}, ( $arg_name => ' ' ) },
					{ %{$mandatory_args}, ( $arg_name => " \n " ) };
			}
		} else {
			push @cases,
				{ %{$mandatory_args}, ( $arg_name => "\n" ) },	# new lines are fun
				{ %{$mandatory_args}, ( $arg_name => ' ' ) },
				{ %{$mandatory_args}, ( $arg_name => " \n " ) };
		}
		if (defined $spec->{max}) {
			my $len = $spec->{max};
			if((!defined($spec->{min})) || ($spec->{min} < ($len - 1))) {
				push @cases, { %{$mandatory_args}, ( $arg_name => rand_str($len - 1) ) };
			} else {
				push @cases, { %{$mandatory_args}, ( $arg_name => rand_str($len - 1), _STATUS => 'DIES' ) };
			}
			push @cases, { %{$mandatory_args}, ( $arg_name => rand_str($len) ) };
			push @cases, { %{$mandatory_args}, ( $arg_name => rand_str($len + 1), _STATUS => 'DIES' ) };
		}
	}

	if(defined $spec->{matches}) {
		my $re = $spec->{matches};

		# --- Positive controls ---
		foreach my $val (@candidate_good) {
			if ($val =~ $re) {
				push @cases, { %{$mandatory_args}, ( $arg_name => $val ) };
				last; # one good match is enough
			}
		}

		# --- Negative controls ---
		foreach my $val (@candidate_bad) {
			if(!defined($val)) {
				push @cases, { _input => undef, _STATUS => 'DIES', _LINE => __LINE__ } if($config{'test_undef'});
			} elsif ($val !~ $re) {
				push @cases, { _input => $val, _STATUS => 'DIES', _LINE => __LINE__ };
			}
		}
		push @cases, { $arg_name => undef, _STATUS => 'DIES' } if($config{'test_undef'});
	}
	if(defined $spec->{nomatch}) {
		my $re = $spec->{nomatch};

		# --- Positive controls ---
		foreach my $val (@candidate_good) {
			if ($val !~ $re) {
				push @cases, { %{$mandatory_args}, ( $arg_name => $val ) };
				last; # one good match is enough
			}
		}

		# --- Negative controls ---
		foreach my $val (@candidate_bad) {
			if ($val =~ $re) {
				push @cases, { $arg_name => $val, _STATUS => 'DIES' };
			}
		}
	}

	[% IF module %]
		# Send wrong data type - builtins aren't good at checking this
		if($config{'test_nuls'}) {
			push @cases,
				{ %{$mandatory_args}, $arg_name => "\0", _STATUS => 'DIES', _LINE => __LINE__ },
				{ %{$mandatory_args}, $arg_name => "nul\0", _STATUS => 'DIES', _LINE => __LINE__ },
				{ %{$mandatory_args}, $arg_name => "\0nul", _STATUS => 'DIES', _LINE => __LINE__ };
			if(defined($spec->{matches})) {
				my $re = $spec->{matches};
				foreach my $val (@candidate_good) {
					$val = "$val\0";
					if ($val !~ $re) {
						push @cases, { %{$mandatory_args}, ( $arg_name => $val, _STATUS => 'DIES', _DESCRIPTION => 'Nul byte after regex' ) };
					}
					$val = "\0$val";
					if ($val !~ $re) {
						push @cases, { %{$mandatory_args}, ( $arg_name => $val, _STATUS => 'DIES', _DESCRIPTION => 'Nul byte before regex' ) };
					}
				}
			}
		}
		if($config{'test_empty'}) {
			push @cases,
				{ %{$mandatory_args}, ( $arg_name => [], _STATUS => 'DIES', _LINE => __LINE__ ) },
				{ %{$mandatory_args}, ( $arg_name => {}, _STATUS => 'DIES', _LINE => __LINE__ ) };
		}
		push @cases,
			{ %{$mandatory_args}, ( $arg_name => \'ref to scalar', _STATUS => 'DIES', _LINE => __LINE__ ) },
			{ %{$mandatory_args}, ( $arg_name => sub { die 'boom' }, _STATUS => 'DIES', _LINE => __LINE__ ) },
			{ %{$mandatory_args}, ( $arg_name => bless({}, 'Evil::Class'), _STATUS => 'DIES', _LINE => __LINE__ ) },
			# { %{$mandatory_args}, ( $arg_name => [1, 2, 3], _STATUS => 'DIES', _LINE => __LINE__ ) },	# Generates false positives. Why?
			{ %{$mandatory_args}, ( $arg_name => { a => 1 }, _STATUS => 'DIES', _LINE => __LINE__ ) };
	[% END %]

	return \@cases;
}

# dedup, fuzzing can easily generate repeats
# FIXME: I don't think this catches them all
# FIXME: Handle cases with Class::Simple calls
# FIXME: The JSON encoding fails on various data types that are sent (e.g. scalar refs, objects) so don't bother
sub _dedup_cases
{
	my $cases = shift;

	# Do not use JSON::MaybeXS because it will fail on non utf-8 characters
	require JSON::PP;
	JSON::PP->import();

	# return inside eval{} returns from the eval block, not from
	# _dedup_cases — the deduped result must be captured here, or
	# this always falls through to the unduplicated $cases below.
	#
	# encode_json() does not sort hash keys, so two structurally
	# identical case hashrefs can serialize with different key order
	# (Perl's per-hash iteration order randomisation) and dodge the
	# %seen check below — use ->canonical to make the dump order-stable.
	my $rc = eval {
		my $json = JSON::PP->new->canonical;
		my %seen;
		my @rc = grep {
			my $dump = $json->encode($_);
			!$seen{$dump}++
		} @{$cases};

		return \@rc;
	};
	# Carp::carp(__PACKAGE__, ": disabling deduping: $@");

	return $@ ? $cases : $rc;
}

sub generate_tests
{
	my $input = $_[0];
	my %mandatory_args = %{$_[1]};

	my @cases;

	foreach my $field (keys %input) {
		my $spec = $input{$field} || {};
		foreach my $field(keys %{$spec}) {
			next if($field =~ /^_/);	# Ignore comments
			if(!grep({ $_ eq $field } ('type', 'min', 'max', 'optional', 'matches', 'can', 'position', 'memberof', 'semantic', 'isa'))) {
				die("TODO: handle schema keyword '$field'");
			}
		}
	}

	# Build a test of the mandatory args
	push @cases, { _input => \%mandatory_args, status => 'OK' } if(keys %mandatory_args);

	for (1..[% iterations_code %]) {
		# One by one change each of the mandatory fields
		foreach my $field (keys %input) {
			my $spec = $input{$field} || {};
			next if $spec->{'memberof'};	# Memberof data is created below
			my $type = $spec->{type} || 'string';

			my %case_input = (%mandatory_args);
			# 1) Sometimes pick a field-specific edge-case
			if (exists $edge_cases{$field} && rand() < PROB_EDGE_CASE) {
				push @cases, { _input => {%case_input}, status => 'OK' } if(keys %case_input);
				$case_input{$field} = _pick_from($edge_cases{$field});
				next;
			}

			# 2) Sometimes pick a type-level edge-case
			if (exists $type_edge_cases{$type} && rand() < 0.3) {
				$case_input{$field} = _pick_from($type_edge_cases{$type});
				push @cases, { _input => \%case_input, status => 'OK' } if(keys %case_input);
				next;
			}

			# 3) Sormal random generation by type
			if ($type eq 'string') {
				if(my $re = $spec->{matches}) {
					if(ref($re) ne 'Regexp') {
						$re = qr/$re/;
					}
					if($spec->{'max'}) {
						$case_input{$field} = Data::Random::String::Matches->create_random_string({ length => $spec->{'max'}, regex => $re });
					} elsif($spec->{'min'}) {
						$case_input{$field} = Data::Random::String::Matches->create_random_string({ length => $spec->{'min'}, regex => $re });
					} else {
						$case_input{$field} = Data::Random::String::Matches->create_random_string({ regex => $re });
					}
				} elsif(my $semantic = $spec->{'semantic'}) {
					if($semantic eq 'email') {
						$case_input{$field} = rand_email($spec->{'max'} // $spec->{'min'});
					} else {
						diag(__LINE__, ": TODO: handle semantic type '$semantic'");
					}
				} elsif(!$spec->{'memberof'}) {
					if(my $min = $spec->{min}) {
						$case_input{$field} = rand_str($min);
						if($config{'test_empty'} && ($min == 0)) {
							push @cases, { _input => {%case_input}, status => 'OK' } if(keys %case_input);
							$case_input{$field} = '';
						}
					} else {
						$case_input{$field} = rand_str();
						if($config{'test_empty'}) {
							push @cases, { _input => {%case_input}, status => 'OK' } if(keys %case_input);
							$case_input{$field} = '';
						}
					}
				}
			} elsif ($type eq 'integer') {
				if(my $min = $spec->{min}) {
					if(my $max = $spec->{'max'}) {
						$case_input{$field} = int(rand($max - $min + 1)) + $min;
					} else {
						$case_input{$field} = rand_int() + $min;
					}
				} elsif(exists($spec->{min})) {
					# min == 0
					if(my $max = $spec->{'max'}) {
						$case_input{$field} = int(rand($max + 1));
					} else {
						$case_input{$field} = int(abs(rand_int()));
					}
				} else {
					push @cases,
						# If it's takes an integer, a float should die
						{ _input => rand_int() + 0.2, _STATUS => 'DIES', _LINE => __LINE__ },
						# Integers passed as a string
						{ _input => "007", _STATUS => 'OK', _LINE => __LINE__ },
						{ _input => "0x10", _STATUS => 'OK', _LINE => __LINE__ },
						{ _input => "1_000", _STATUS => 'OK', _LINE => __LINE__ };
					$case_input{$field} = rand_int();
				}
			} elsif ($type eq 'boolean') {
				$case_input{$field} = rand_bool();
			} elsif ($type eq 'number') {
				if(defined(my $min = $spec->{min})) {
					$case_input{$field} = abs(rand_num()) + $min;
				} else {
					$case_input{$field} = rand_num();
				}
			} elsif ($type eq 'arrayref') {
				my $circular_ref = [];
				push @{$circular_ref}, $circular_ref;

				push @cases, { %mandatory_args, ($field => $circular_ref, _STATUS => 'DIES', _LINE => __LINE__, _DESCRIPTION => "Don't accept array ref with circular references") };

				if(my $element_type = $input{element_type}) {
					if($element_type eq 'integer') {
						push @cases,
							{ %mandatory_args, ($field => '0.3', _STATUS => 'DIES', _LINE => __LINE__, _DESCRIPTION => 'float in list of integers') },
							{ %mandatory_args, ($field => 'string in array of ints', _STATUS => 'DIES', _LINE => __LINE__, _DESCRIPTION => 'string in list of integers') };
					} elsif($element_type eq 'boolean') {
						push @cases,
							{ %mandatory_args, ($field => 2, _STATUS => 'DIES', _LINE => __LINE__, _DESCRIPTION => '2 list of booleans') },
							{ %mandatory_args, ($field => '0.3', _STATUS => 'DIES', _LINE => __LINE__, _DESCRIPTION => 'float in list of booleans') },
							{ %mandatory_args, ($field => 'string in array of bools', _STATUS => 'DIES', _LINE => __LINE__, _DESCRIPTION => 'string in list of booleans') };
					}
				}

				push @cases, { %mandatory_args, (field => [ 'fred', undef, 'wilma' ], _LINE => __LINE__, _DESCRIPTION => 'undef element in array') } if($config{test_undef});
				$case_input{$field} = rand_arrayref();
			} elsif ($type eq 'hashref') {
				if($config{test_undef}) {
					push @cases, { %mandatory_args, (field => { 'fred' => 'wilma', 'undef' => undef, 'foo' => 'bar' }, _LINE => __LINE__, _DESCRIPTION => 'undef element in hashref') };
					push @cases, { %mandatory_args, (field => { 'fred' => 'wilma', 'nested' => { 'first' => 'one', 'undef' => undef, 'third' => 'three' }, 'foo' => 'bar' }, _LINE => __LINE__, _DESCRIPTION => 'undef element in nested hashref') };
				}
				$case_input{$field} = rand_hashref();
			} elsif($config{'test_undef'}) {
				$case_input{$field} = undef;
			}

			# 4) occasionally drop optional fields
			if ($spec->{optional} && rand() < 0.25) {
				delete $case_input{$field};
			}
			push @cases, { _input => \%case_input, status => 'OK' } if(keys %case_input);

			#####################
			# FIXME: Start of duplicated code from the above part of the loop.
			#	Go through and remove duplications then remove this code
			if (exists $spec->{memberof} && ref $spec->{memberof} eq 'ARRAY' && @{$spec->{memberof}}) {
				# Generate edge cases for memberof
				# inside values
				foreach my $val (@{$input{memberof}}) {
					push @cases,
						{ %mandatory_args, ( $field => $val ) },
						{ %mandatory_args, ( $field => " $val", _LINE => __LINE__, _STATUS => 'DIES' ) },
						{ %mandatory_args, ( $field => "$val ", _LINE => __LINE__, _STATUS => 'DIES' ) },
						{ %mandatory_args, ( $field => substr($val, 0, -1), _LINE => __LINE__, _STATUS => 'DIES' ) };
					if($val =~ /[A-Z]/) {
						push @cases, { %mandatory_args, ( $field => lc($val), _LINE => __LINE__, _STATUS => 'DIES' ) };
					}
					if($val =~ /[a-z]/) {
						push @cases, { %mandatory_args, ( $field => uc($val), _LINE => __LINE__, _STATUS => 'DIES' ) };
					}
				}
				# outside value
				my $outside;
				if ($type eq 'integer' || $type eq 'number') {
					$outside = (sort { $a <=> $b } @{$spec->{memberof}})[-1] + 1;
				} else {
					$outside = 'INVALID_MEMBEROF';
				}
				push @cases, { %mandatory_args, ( $field => $outside, _STATUS => 'DIES' ) };
			} else {
				# Generate edge cases for min/max
				if($type eq 'integer') {
					push @cases, @{_generate_integer_cases($field, $spec, \%mandatory_args, _LINE => __LINE__)};
				} elsif(($type eq 'number') || ($type eq 'float')) {
					push @cases, @{_generate_float_cases($field, $spec, \%mandatory_args, _LINE => __LINE__)};
				} elsif($type eq 'string') {
					push @cases, @{_generate_string_cases($field, $spec, \%mandatory_args)};
				} elsif ($type eq 'arrayref') {
					if (defined $spec->{min}) {
						my $len = $spec->{min};
						push @cases,
							{ %mandatory_args, $field => [ (1) x ($len + 1) ] },	# just inside
							{ %mandatory_args, $field => [ (1) x $len ] };	# border
						push @cases, { %mandatory_args, $field => [ (1) x ($len - 1) ], _STATUS => 'DIES' } if $len > 0; # outside
					} else {
						push @cases, { %mandatory_args, $field => [] } if($config{'test_empty'});	# No min, empty array should be allowable
					}
					if (defined $spec->{max}) {
						my $len = $spec->{max};
						push @cases,
							{ %mandatory_args, $field => [ (1) x ($len - 1) ] },	# just inside
							{ %mandatory_args, $field => [ (1) x $len ] },	# border
							{ %mandatory_args, $field => [ (1) x ($len + 1) ], _STATUS => 'DIES' }; # outside
					} elsif((defined $spec->{min}) || ($spec->{min} <= 3)) {
						push @cases, { %mandatory_args, $field => [ 'first', undef, 'third' ], _DESCRIPTION => 'undef in an arrayref', _LINE => __LINE__ } if($config{test_undef});
					}
				} elsif ($type eq 'hashref') {
					if (defined $spec->{min}) {
						my $len = $spec->{min};
						push @cases,
							{ %mandatory_args, $field => { map { "k$_" => 1 }, 1 .. ($len + 1) } },
							{ %mandatory_args, $field => { map { "k$_" => 1 }, 1 .. $len } };
						push @cases, { %mandatory_args, $field => { map { "k$_" => 1 }, 1 .. ($len - 1) }, _STATUS => 'DIES' } if $len > 0;
					} else {
						push @cases, { %mandatory_args, $field => {} } if($config{'test_empty'});	# No min, empty hash should be allowable
					}
					if (defined $spec->{max}) {
						my $len = $spec->{max};
						push @cases,
							{ %mandatory_args, $field => { map { "k$_" => 1 }, 1 .. ($len - 1) } },
							{ %mandatory_args, $field => { map { "k$_" => 1 }, 1 .. $len } },
							{ %mandatory_args, $field => { map { "k$_" => 1 }, 1 .. ($len + 1) }, _STATUS => 'DIES' };
					}
				} elsif ($type eq 'boolean') {
					push @cases, @{_generate_boolean_cases($field, $spec, \%mandatory_args)};
				}
			}

			# case_sensitive tests for memberof
			if (defined $spec->{memberof} && exists $spec->{case_sensitive}) {
				if (!$spec->{case_sensitive}) {
					# Generate mixed-case versions of memberof values
					foreach my $val (@{$spec->{memberof}}) {
						push @cases, { %mandatory_args, ( $field => uc($val) ) },
							{ %mandatory_args, ( $field => lc($val) ) },
							{ %mandatory_args, ( $field => ucfirst(lc($val)) ) };
					}
				}
			}

			# Add notmemberof tests
			if (defined $spec->{notmemberof}) {
				my @blacklist = @{$spec->{notmemberof}};
				# Each blacklisted value should die
				foreach my $val (@blacklist) {
					push @cases, { %mandatory_args, ( $field => $val, _STATUS => 'DIES' ) };
				}
				# Non-blacklisted value should pass
				push @cases, { %mandatory_args, ( $field => '_not_in_blacklist_' ) };
			}

			# semantic tests
			if(defined(my $semantic = $spec->{'semantic'})) {
				if($semantic eq 'unix_timestamp') {
					push @cases, { %mandatory_args, ( -1, _STATUS => 'DIES' ) },
						{ %mandatory_args, ( 0 ) },
						{ %mandatory_args, ( 1 ) },
						{ %mandatory_args, ( time ) },
						{ %mandatory_args, ( 2147483647 ) },
						{ %mandatory_args, ( 45.67, _STATUS => 'DIES', _DESCRIPTION => 'UNIX timestamp should not be a float' ) },
						{ %mandatory_args, ( 2147483648, _STATUS => 'DIES' ) };
				} else {
					diag("semantic type $semantic is not yet supported");
				}
			}
			if(@relationships) {
				diag('Run relationship tests') if($ENV{'TEST_VERBOSE'});

				foreach my $rel (@relationships) {
					my $type = $rel->{type};

					if($type eq 'mutually_exclusive') {
						my ($p1, $p2) = @{ $rel->{params} };
						# Both specified — should die
						run_test(
							{ _STATUS => 'DIES',
							  _DESCRIPTION => "mutually exclusive: $p1 and $p2 both given" },
							{ %mandatory_args, $p1 => 'val1', $p2 => 'val2' },
							\%output,
							$positions
						);
						# Each alone — should live
						run_test(
							{ _DESCRIPTION => "mutually exclusive: only $p1 given" },
							{ %mandatory_args, $p1 => 'val1' },
							\%output,
							$positions
						);
						run_test(
							{ _DESCRIPTION => "mutually exclusive: only $p2 given" },
							{ %mandatory_args, $p2 => 'val2' },
							\%output,
							$positions
						);

					} elsif($type eq 'required_group') {
						my @params = @{ $rel->{params} };
						# None specified — should die
						my %none = map { $_ => undef } @params;
						run_test(
							{ _STATUS => 'DIES',
							  _DESCRIPTION => 'required_group: none of (' . join(', ', @params) . ') given' },
							{ %mandatory_args, %none },
							\%output,
							$positions
						);
						# Each alone — should live
						foreach my $p (@params) {
							run_test(
								{ _DESCRIPTION => "required_group: only $p given" },
								{ %mandatory_args, $p => 'val' },
								\%output,
								$positions
							);
						}

					} elsif($type eq 'conditional_requirement') {
						my ($if_param, $then_param) = ($rel->{if}, $rel->{then_required});
						# if_param set, then_param missing — should die
						run_test(
							{ _STATUS => 'DIES',
							  _DESCRIPTION => "conditional: $if_param set but $then_param missing" },
							{ %mandatory_args, $if_param => 'val', $then_param => undef },
							\%output,
							$positions
						);
						# Both set — should live
						run_test(
							{ _DESCRIPTION => "conditional: $if_param and $then_param both set" },
							{ %mandatory_args, $if_param => 'val', $then_param => 'val' },
							\%output,
							$positions
						);
						# if_param absent — then_param absence should not matter
						run_test(
							{ _DESCRIPTION => "conditional: $if_param absent so $then_param not required" },
							{ %mandatory_args, $then_param => undef },
							\%output,
							$positions
						);

					} elsif($type eq 'dependency') {
						my ($param, $requires) = ($rel->{param}, $rel->{requires});
						# param set, requires missing — should die
						run_test(
							{ _STATUS => 'DIES',
							  _DESCRIPTION => "dependency: $param set but $requires missing" },
							{ %mandatory_args, $param => 'val', $requires => undef },
							\%output,
							$positions
						);
						# Both set — should live
						run_test(
							{ _DESCRIPTION => "dependency: $param and $requires both set" },
							{ %mandatory_args, $param => 'val', $requires => 'val' },
							\%output,
							$positions
						);

					} elsif($type eq 'value_constraint') {
						my ($if_param, $then_param, $op, $val) =
							($rel->{if}, $rel->{then}, $rel->{operator}, $rel->{value});
						# if_param set, then_param wrong value — should die
						my $wrong_val = ($op eq '==') ? $val + 1 : $val - 1;
						run_test(
							{ _STATUS => 'DIES',
							  _DESCRIPTION => "value_constraint: $if_param set, $then_param = $wrong_val (wrong)" },
							{ %mandatory_args, $if_param => 'val', $then_param => $wrong_val },
							\%output,
							$positions
						);
						# if_param set, then_param correct value — should live
						run_test(
							{ _DESCRIPTION => "value_constraint: $if_param set, $then_param = $val (correct)" },
							{ %mandatory_args, $if_param => 'val', $then_param => $val },
							\%output,
							$positions
						);

					} elsif($type eq 'value_conditional') {
						my ($if_param, $equals, $then_param) =
							($rel->{if}, $rel->{equals}, $rel->{then_required});
						# if_param equals trigger value, then_param missing — should die
						run_test(
							{ _STATUS => 'DIES',
							  _DESCRIPTION => "value_conditional: $if_param='$equals', $then_param missing" },
							{ %mandatory_args, $if_param => $equals, $then_param => undef },
							\%output,
							$positions
						);
						# if_param equals trigger value, then_param present — should live
						run_test(
							{ _DESCRIPTION => "value_conditional: $if_param='$equals', $then_param present" },
							{ %mandatory_args, $if_param => $equals, $then_param => 'val' },
							\%output,
							$positions
						);
						# if_param different value — then_param absence should not matter
						run_test(
							{ _DESCRIPTION => "value_conditional: $if_param != '$equals', $then_param not required" },
							{ %mandatory_args, $if_param => '__other__', $then_param => undef },
							\%output,
							$positions
						);
					}
				}
			}
		}
	}

	return \@cases;
}

sub populate_positions
{
	my $input = shift;

	my $rc;
	foreach my $arg (keys %{$input}) {
		my $spec = $input->{$arg} || {};
		if(((ref($spec)) eq 'HASH') && defined($spec->{'position'})) {
			$rc->{$arg} = $spec->{'position'};
		} else {
			if($rc) {
				::diag("$arg is missing a position parameter in its schema");
			}
			return;	# All must be defined
		}
	}

	return $rc;
}

sub run_test
{
	my($case, $input, $output, $positions) = @_;

	if($ENV{'TEST_VERBOSE'}) {
		diag('input: ', Dumper($input));
	}

	my $name = delete local $case->{'_NAME'};
	my $properties = delete local $case->{_PROPERTIES};
	my $description = delete local $case->{_DESCRIPTION};
	my $result;
	my $mess;
	my @alist = ();
	if(defined($input) && !ref($input)) {
		# $mess is later used as a sprintf() format string further
		# below — a literal '%' in $name/$input must be escaped to
		# '%%' first, the same as the aggregate branch does for
		# $args, or a value like '%s' / '%n' corrupts the sprintf call.
		(my $safe_input = $input) =~ s/%/%%/g;
		if($name) {
			(my $safe_name = $name) =~ s/%/%%/g;
			$mess = "[% function %]($safe_name = '$safe_input') %s";
		} else {
			$mess = "[% function %]('$safe_input') %s";
		}
	} elsif(defined($input)) {
		if($positions) {
			# Positional args
			foreach my $key (keys %{$input}) {
				if(($key ne '_STATUS') && ($key ne '_NAME') && ($key ne '_LINE') && ($key ne '_PROPERTIES') && ($key ne '_DESCRIPTION')) {
					if(exists($positions->{$key})) {
						$alist[$positions->{$key}] = delete $input->{$key};
					} else {
						diag("Lost position number for $key");
					}
				}
			}
			@alist = grep { defined $_ } @alist;	# Undefs will cause not enough args to be sent, which is a nice test
			$input = join(', ', @alist);
		} else {
			# Named args
			if(ref($input) ne 'HASH') {
				if($case->{'_STATUS'} ne 'DIES') {
					die('Input is missing list of arguments (perhaps you only listed types)');
				}
				# e.g., we are passing a ref to a scalar to something that only takes a scalar, so it should cause the routine to die
				$positions = {};
				$alist[0] = $input;
			} else {
				foreach my $key (sort keys %{$input}) {
					if(($key ne '_STATUS') && ($key ne '_NAME') && ($key ne '_LINE') && ($key ne '_PROPERTIES')) {
						if(defined($input->{$key})) {
							push @alist, "'$key' => '$input->{$key}'";
						} else {
							push @alist, "'$key' => undef";
						}
					}
				}
			}
		}
		my $args = join(', ', @alist);
		$args =~ s/%/%%/g;
		$mess = "[% function %]($args) %s";
	} else {
		$mess = "[% function %] %s";
	}

	my $status = delete $case->{'_STATUS'} || $output->{'_STATUS'};
	my $line = delete $case->{'_LINE'};
	my %ENV_before = %ENV;
	my $cwd_before = Cwd::getcwd();
	my %cwd_files_before = map { $_ => 1 } do { opendir(my $dh, '.') or die $!; readdir($dh) };

	local $SIG{ALRM} = sub { die '__TIMEOUT__' };
	if((!defined($config{timeout})) || ($config{timeout} > 0)) {
		alarm($config{'timeout'} // 10);
	}
	my $old_warn = $SIG{__WARN__};
	my $old_die = $SIG{__DIE__};
	my $ok = eval {
		if(defined($status)) {
			if($status eq 'DIES') {
				my $err;
				if($positions) {
					[% IF position_code %]
						if(defined($name)) {
							dies_ok { [% position_code %] } sprintf($mess, "dies (position test) - $name (status = DIES)");
						} else {
							dies_ok { [% position_code %] } sprintf($mess, 'dies (position test, status = DIES)');
						}
						$err = $@;
					[% ELSE %]
						ok(0, 'dies: position_code not defined');
					[% END %]
				} else {
					dies_ok { [% call_code %] } sprintf($mess, 'dies');
					$err = $@;
					ok(!defined($result));
				}
				ok(defined($err));
				ok(length($err));
				ok(!ref($err));
				if(defined($name)) {
					unlike($err, qr/unitialized/, "$name doesn't involve an uninitialized variable");
				} else {
					unlike($err, qr/unitialized/, "Test doesn't involve an uninitialized variable");
				}
				return;	# There should be no output to validate
			} elsif($status eq 'WARNS') {
				warnings_exist { [% call_code %] } qr/./, sprintf($mess, 'warns');
			} else {
				die 'TODO: properties' if(scalar keys %{$properties});
				if($positions) {
					[% IF position_code %]
						if(defined($name)) {
							lives_ok { [% position_code %] } sprintf($mess, "survives (position test) - $name (status = LIVES)");
						} else {
							lives_ok { [% position_code %] } sprintf($mess, 'survives (position test, status = LIVES)');
						}
					[% ELSE %]
						ok(0, 'position_code not defined');
					[% END %]
				} elsif(defined($description)) {
					lives_ok { [% call_code %] } sprintf($mess, "survives ($description; status = LIVES)");
				} elsif(defined($line)) {
					lives_ok { [% call_code %] } sprintf($mess, "survives (line = $line; status = LIVES)");
				} else {
					lives_ok { [% call_code %] } sprintf($mess, 'survives (status = LIVES)');
				}
				if($properties->{idempotent}) {
					[% determinism_code %]
				}
			}
		} elsif($positions) {
			if(defined($name)) {
				lives_ok { [% position_code %] } sprintf($mess, "survives (position test) - $name");
				if($properties->{idempotent} && (scalar(@alist) == 1)) {
					[% UNLESS module %]
						ok([% function %]($alist[0]) eq [% function %]([% function %]($alist[0])), 'function is idempotent');
						ok([% function %]($alist[0]) eq [% function %]($alist[0]), 'function is idempotent');
					[% END %]
				}
			} else {
				die 'TODO: properties' if(scalar keys %{$properties});
				lives_ok { [% position_code %] } sprintf($mess, 'survives (position test)');

				# An extra argument should be ignored, except for getsetters, so only test if there's more than one arg
				if(scalar(@alist) > 1) {
					push(@alist, 'foo');
					lives_ok { [% position_code %] } sprintf($mess, 'survives (position test, with extra argument)');
				}
			}
			if($properties->{idempotent}) {
				[% determinism_code %]
			}
		} else {
			# Status not given, assume set to LIVES
			die 'TODO: properties' if(scalar keys %{$properties});
			if(defined($description)) {
				lives_ok { [% call_code %] } sprintf($mess, "survives ($description)");
			} elsif(defined($line)) {
				lives_ok { [% call_code %] } sprintf($mess, "survives (line = $line)");
			} else {
				lives_ok { [% call_code %] } sprintf($mess, 'survives');
			}
			if($properties->{idempotent}) {
				[% determinism_code %]
			}
		}
		1;
	};

	alarm 0;

	# Clean up any files the function created in cwd
	for my $f (do { opendir(my $dh, '.') or die $!; readdir($dh) }) {
		unlink $f if !$cwd_files_before{$f} && -f $f;
	}

	# Check the test did not timeout
	diag($@) if($@);
	ok((!defined($@)) || (length($@) == 0));

	# Global side effect detection
	is(Cwd::getcwd(), $cwd_before, 'cwd not modified');
	is_deeply(_filtered_env(\%ENV), _filtered_env(\%ENV_before), 'ENV not modified');
	is($SIG{__WARN__}, $old_warn, 'warn handler is not changed');
	is($SIG{__DIE__}, $old_die, 'die handler is not changed');

	delete local $output->{'_STATUS'};

	if(scalar keys %{$output}) {
		if($ENV{'TEST_VERBOSE'}) {
			diag('result: ', Dumper($result));
		}
		returns_ok($result, $output, 'output validates');
		if(((!defined($status)) || ($status eq 'OK')) && defined($result)) {
			is(
				Unicode::Normalize::NFC($result),
				Unicode::Normalize::NFC(Unicode::Normalize::NFD($result)),
				'Unicode normalization stable'
			);
		}
	}
}

# On Windows $PWD etc are not stable
sub _filtered_env {
	my %env = %{ shift() };

	if($^O eq 'MSWin32') {
		delete @env{
			qw(
				PWD
				OLDPWD
				_
				SHLVL
			)
		};
	}

	return \%env;
}

diag('Run Fuzz Tests') if($ENV{'TEST_VERBOSE'});

foreach my $case (@{fuzz_inputs()}) {
	# my %params;
	# lives_ok { %params = get_params(\%input, %$case) } 'Params::Get input check';
	# lives_ok { validate_strict(\%input, %params) } 'Params::Validate::Strict input check';

	my $input;
	if((ref($case) eq 'HASH') && exists($case->{'_input'})) {
		$input = $case->{'_input'};
	} else {
		$input = $case;
	}

	if(my $line = ($case->{'_LINE'} || $input{'_LINE'})) {
		diag("Test case from line number $line") if($ENV{'TEST_VERBOSE'});
	}
	if(my $description = ($case->{'_DESCRIPTION'} || $input{'_DESCRIPTION'})) {
		diag("Test case $description") if($ENV{'TEST_VERBOSE'});
	}

	{
		# local %ENV;
		run_test($case, $input, \%output, $positions);
		# delete $ENV{'LANG'};
		# delete $ENV{'LC_ALL'};
		# run_test($case, $input, \%output, $positions);
		# $ENV{'LANG'} = 'fr_FR.utf8';
		# $ENV{'LC_ALL'} = 'fr_FR.utf8';
		# run_test($case, $input, \%output, $positions);
	}
}

if(scalar(keys %transforms)) {
	diag('Run ', scalar(keys %transforms), ' transform tests');
}
# diag('-' x 60);

# Build the foundation - which is a basic test with sensible defaults in the field
foreach my $transform (keys %transforms) {
	my $foundation = _fill_foundation();	# basic set of data with every field filled in with a sensible default value

	# The foundation should work
	my $case = { _NAME => "basic $transform test", _LINE => __LINE__ };
	my $positions = populate_positions(\%input);
	run_test($case, $foundation, \%output, $positions);

	# Generate transform tests
	# Don't generate invalid data, that's all already done,
	#	this is about verifying the transorms
	my @tests;
	diag("tests for transform $transform") if($ENV{'TEST_VERBOSE'});

	# Now modify the foundation with test code

	# BUILD CODE TO CALL FUNCTION
	# CALL FUNCTION
	# CHECK STATUS CORRECT
	# IF STATUS EQ LIVES
	#	CHECK OUTPUT USING returns_ok
	# FI

	my $transform_input = $transforms{$transform}{'input'} || {};

	foreach my $field (keys %input) {
		my $spec = $transform_input->{$field} || {};
		my $type = $spec->{type} || 'string';

		# If there's a specific value, test that exact value
		if (exists $spec->{value}) {
			push @tests, {
				%{$foundation},
				$field => $spec->{value},
				_LINE => __LINE__,
				_DESCRIPTION => "$transform: $field=$spec->{value}"
			};
			next;
		}

		# Generate edge cases based on type and contraints
		if($type eq 'integer') {
			push @tests, @{_generate_integer_cases($field, $spec, $foundation)};
		} elsif(($type eq 'number') || ($type eq 'float')) {
			push @tests, @{_generate_float_cases($field, $spec, $foundation)};
		} elsif($type eq 'string') {
			push @tests, @{_generate_string_cases($field, $spec, $foundation)};
		} elsif($type eq 'boolean') {
			push @tests, @{_generate_boolean_cases($field, $spec, $foundation)};
		} elsif ($type eq 'arrayref') {
			if(defined $spec->{min}) {
				push @tests, { %{$foundation}, ( $field => rand_arrayref($spec->{min} + 1) ) };	# just inside
				push @tests, { %{$foundation}, ( $field => rand_arrayref($spec->{min}) ) };	# border
			} else {
				push @tests, { %{$foundation}, ( $field => rand_arrayref() ) };
			}
			if(defined $spec->{max}) {
				push @tests, { %{$foundation}, ( $field => rand_arrayref($spec->{max} - 1) ) };	# just inside
				if((defined $spec->{min}) && ($spec->{'min'} != $spec->{'max'})) {
					push @tests, { %{$foundation}, ( $field => rand_arrayref($spec->{max}) ) };	# border
				}
			} elsif((defined $spec->{min}) || ($spec->{min} <= 3)) {
				push @tests, { %{$foundation}, ( $field => [ 'foo', undef, 'bar' ] ), _DESCRIPTION => 'undef in an arrayref', _LINE => __LINE__ } if($config{test_undef});
			}
		} else {
			die("TODO: transform type $type for test case");
		}
	}

	if($config{dedup}) {
		@tests = @{_dedup_cases(\@tests)};
	}

	{
		# local %ENV;
		my $transform_output = $transforms{$transform}{'output'} || {};
		my $properties_array_ref = $transforms{$transform}{properties};

		my $properties = {};
		if($properties_array_ref) {
			$properties->{idempotent} = (grep { $_ eq 'idempotent'} @{$properties_array_ref}) ? 1 : 0;
		}

		foreach my $test(@tests) {
			if(my $line = (delete $test->{'_LINE'} || delete $input{'_LINE'})) {
				diag("Test case from line number $line") if($ENV{'TEST_VERBOSE'});
			}

			run_test({ _NAME => $transform, _PROPERTIES => $properties }, $test, $transform_output, $positions);
			# delete $ENV{'LANG'};
			# delete $ENV{'LC_ALL'};
			# run_test({ _NAME => $transform }, $test, \%output, $positions);
			# $ENV{'LANG'} = 'de_DE.utf8';
			# $ENV{'LC_ALL'} = 'de_DE.utf8';
			# run_test({ _NAME => $transform }, $test, \%output, $positions);
		}
	}
}

sub _fill_foundation
{
	my $foundation;

	foreach my $field (keys %input) {
		my $spec = $input{$field} || {};
		my $type = $spec->{type} || 'string';

		if(($type eq 'number') || ($type eq 'float')) {
			if(defined $spec->{min}) {
				if(defined $spec->{max}) {
					$foundation->{$field} = $spec->{max};	# border
				} else {
					$foundation->{$field} = abs(rand_num()) + $spec->{'min'};
				}
			} else {
				if(defined $spec->{max}) {
					$foundation->{$field} = $spec->{max};	# border
				} else {
					$foundation->{$field} = -0.01;	# No min, so -0.01 should be allowable
				}
			}
		} elsif($type eq 'string') {
			if(defined $spec->{min} && $spec->{min} > 0) {
				$foundation->{$field} = rand_str($spec->{min});
			} elsif(defined $spec->{max} && $spec->{max} > 0) {
				$foundation->{$field} = rand_str($spec->{max});
			} else {
				$foundation->{$field} = 'test_value';
			}
		} elsif ($type eq 'integer') {
			if (defined $spec->{min}) {
				$foundation->{$field} = $spec->{min};
			} elsif (defined $spec->{max}) {
				$foundation->{$field} = rand_int() + $spec->{max};
			} else {
				$foundation->{$field} = rand_int();
			}
		} elsif ($type eq 'boolean') {
			$foundation->{$field} = 1;
		} elsif ($type eq 'arrayref') {
			$foundation->{$field} = rand_arrayref(defined($spec->{'min'}) ? $spec->{'min'} : ($spec->{'max'} // 5));
		} elsif ($type eq 'hashref') {
			$foundation->{$field} = { key => 'value' };
		} else {
			die("TODO: transform type $type for foundation");
		}
	}
	return $foundation;
}

[% IF use_properties %]
# ============================================================
# Property-Based Transform Tests (Test::LectroTest)
# ============================================================

use Test::LectroTest::Compat;
use Test::LectroTest::Generator qw(:common);
use Scalar::Util qw(looks_like_number);

diag('Run property-based transform tests') if($ENV{'TEST_VERBOSE'});

[% transform_properties_code %]

[% END %]

[% corpus_code %]

done_testing();

1;

package MyTestPackage;

sub new { return bless {}, 'MyTestPackage' }

1;
__END__
