The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/env perl

use Test::More tests => 103;
use MQUL qw/doc_matches/;
use Try::Tiny;

# start by making sure doc_matches() fails when it needs to:
my $err = try { doc_matches() } catch { (m/(.+) at t\/01-querying.t/)[0] };
is($err, 'MQUL::doc_matches() requires a document hash-ref.', 'doc_matches() fails when no document is given.');
undef $err;

$err = try { doc_matches('asdf') } catch { (m/(.+) at t\/01-querying.t/)[0] };
is($err, 'MQUL::doc_matches() requires a document hash-ref.', 'doc_matches() fails when a scalar is given for a document.');
undef $err;

$err = try { doc_matches([1,2,3]) } catch { (m/(.+) at t\/01-querying.t/)[0] };
is($err, 'MQUL::doc_matches() requires a document hash-ref.', 'doc_matches() fails when a non-hash reference is given for a document.');
undef $err;

$err = try { doc_matches({ asdf => 1 }, 1) } catch { (m/(.+) at t\/01-querying.t/)[0] };
is($err, 'MQUL::doc_matches() expects a query hash-ref.', 'doc_matches() fails when a scalar is given for the query.');
undef $err;

$err = try { doc_matches({ asdf => 1 }, [1,2,3]) } catch { (m/(.+) at t\/01-querying.t/)[0] };
is($err, 'MQUL::doc_matches() expects a query hash-ref.', 'doc_matches() fails when a non-hash reference is given for the query.');
undef $err;

# let's make sure every document will match an empty query
ok(doc_matches({ asdf => 1 }), 'doc_matches() returns true when no query is given.');
ok(doc_matches({ one => 1, two => 2 }, {}), 'doc_matches() returns true when an empty query is given.');

# let's get to actual querying and start with simple equality and regex checks
ok(doc_matches({ string => 'yo yo', integer => 123 }, { string => 'yo yo' }), 'simple equality works');
ok(!doc_matches({ string => 'my name is nobody' }, { string => 'yo yo' }), 'simple equality does not match erroneously');
ok(doc_matches({ string => 'my name is nobody' }, { string => qr/nobody$/ }), 'simple regex works');
ok(!doc_matches({ string => 'yo yo' }, { string => qr/nobody$/ }), 'simple regex does not match erroneously');

# let's see if deep equality works
ok(doc_matches({ hash => { one => 1, two => 2 }, array => [1,2,3] }, { hash => { one => 1, two => 2 } }), 'deep hash equality works');
ok(doc_matches({ hash => { one => 1, two => 2 }, array => [1,2,3] }, { array => [1,2,3] }), 'deep array equality works');
ok(doc_matches({ deep_hash => { nest => { bird => 'and stuff' } }, number => 123 }, { number => 123, deep_hash => { nest => { bird => 'and stuff' } } }), 'really deep hash works');

# now let's take a look at non-equality and $eq-style equality
ok(doc_matches({ should_eq => 'clint', should_not_eq => 'westwood' }, { should_eq => { '$eq' => 'clint' }, should_not_eq => { '$ne' => 'eastwood' } }), 'simple non-equality works');

# okay, now we're gonna check every advanced operator one at a time
# 1. $gt
ok(doc_matches({ number => 2 }, { number => { '$gt' => 1 } }), 'simple $gt works');
ok(!doc_matches({ number => 2 }, { number => { '$gt' => 3 } }), 'simple $gt does not match erroneously');
# 2. $gte
ok(doc_matches({ float => 12.5, integer => 23 }, { float => { '$gte' => 11 }, integer => { '$gte' => 23 } }), 'simple $gte works');
ok(!doc_matches({ float => 12.5, integer => 23 }, { float => { '$gte' => 13 }, integer => { '$gte' => 23 } }), 'simple $gte does not match erroneously');
# 3. $lt
ok(doc_matches({ number => 4 }, { number => { '$lt' => 5 } }), 'simple $lt works');
ok(!doc_matches({ number => 2 }, { number => { '$lt' => 2 } }), 'simple $lt does not match erroneously');
# 4. $lte
ok(doc_matches({ number => 4 }, { number => { '$lte' => 4 } }), 'simple $lte works');
ok(!doc_matches({ integer => 10, float => 5.124 }, { integer => { '$lte' => 8 }, float => { '$lte' => 5.124 } }), 'simple $lte does not match erroneously');
# 5. $exists
ok(doc_matches({ now => 'for', something => 'completely' }, { something => { '$exists' => 1 } }), 'simple $exists works');
ok(!doc_matches({ now => 'for', something => 'completely' }, { different => { '$exists' => 1 } }), 'simple $exists does not match erroneously');
# 6. not $exists
ok(doc_matches({ now => 'for', something => 'completely' }, { different => { '$exists' => 0 } }), 'simple not $exists works');
ok(!doc_matches({ now => 'for', something => 'completely' }, { something => { '$exists' => 0 } }), 'simple not $exists does not match erroneously');
# 7. $mod
ok(doc_matches({ two => 2, three => 3 }, { two => { '$mod' => [2, 0] }, three => { '$mod' => [2, 1] } }), 'simple $mod works');
ok(!doc_matches({ five => 5 }, { five => { '$mod' => [2, 0] } }), 'simple $mod does not match erroneously');
# 8. $in
ok(doc_matches({ monty => 'python' }, { monty => { '$in' => [qw/cobra python viper/] } }), 'simple $in works');
ok(!doc_matches({ age => 23 }, { age => { '$in' => [1 .. 20] } }), 'simple $in does not match erroneously');
# 9. $nin
ok(doc_matches({ monty => 'python' }, { monty => { '$nin' => [qw/cobra viper asp/] } }), 'simple $nin works');
ok(!doc_matches({ monty => 'python' }, { monty => { '$nin' => [qw/python viper cobra asp/] } }), 'simple $nin does not match erroneously');
# 10. $size
ok(doc_matches({ array => [1 .. 10] }, { array => { '$size' => 10 } }), 'simple $size works');
ok(!doc_matches({ array => [1] }, { array => { '$size' => 10 } }), 'simple $size does not match erroneously');
# 11. $all
ok(doc_matches({ snakes => [qw/python asp cobra viper/] }, { snakes => { '$all' => [qw/python cobra/] } }), 'simple $all works');
ok(!doc_matches({ snakes => [qw/python asp/] }, { snakes => { '$all' => [qw/python asp rattler/] } }), 'simple $all does not match erroneously');
# 12. $type
ok(doc_matches({ integer => 20 }, { integer => { '$type' => 'int' } }), 'positive integers match');
ok(doc_matches({ integer => -20 }, { integer => { '$type' => 'int' } }), 'negative integers match');
ok(doc_matches({ whole => 0 }, { whole => { '$type' => 'whole' } }), 'whole numbers match');
ok(!doc_matches({ whole => -2 }, { whole => { '$type' => 'whole' } }), 'negative integers do not match as wholes');
ok(doc_matches({ float => +1.23e99 }, { float => { '$type' => 'float' } }), 'positive floats match');
ok(doc_matches({ float => -1.23e99 }, { float => { '$type' => 'float' } }), 'negative floats match');
ok(doc_matches({ real => 12.51 }, { real => { '$type' => 'real' } }), 'positive real numbers match');
ok(doc_matches({ real => -12.51 }, { real => { '$type' => 'real' } }), 'negative real numbers match');
ok(doc_matches({ string => 'this is a string' }, { string => { '$type' => 'string' } }), 'strings match');
ok(doc_matches({ array => [1 .. 4] }, { array => { '$type' => 'array' } }), 'arrays match');
ok(doc_matches({ hash => { one => 1, two => 2 } }, { hash => { '$type' => 'hash' } }), 'hashes match');
ok(doc_matches({ bool => 1 }, { bool => { '$type' => 'bool' } }), 'booleans match');
ok(doc_matches({ date => '2003-02-15T13:50:05-05:00' }, { date => { '$type' => 'date' } }), 'w3c formatted dates match');
ok(doc_matches({ null => undef }, { null => { '$type' => 'null' } }), 'nulls (undefs) match');
ok(doc_matches({ regex => qr/\d+/ }, { regex => { '$type' => 'regex' } }), 'regexes match');
ok(!doc_matches({ float => 20.51 }, { float => { '$type' => 'int' } }), "floats don't match as integers");
ok(doc_matches({ number => 0x1234 }, { number => { '$type' => 'string' } }), "numbers can match as strings");
ok(!doc_matches({ array => [1 .. 5] }, { array => { '$type' => 'hash' } }), "arrays don't match as hashes");
ok(!doc_matches({ date => '12.06.1984' }, { date => { '$type' => 'date' } }), "improperly formatted dates don't match");
ok(!doc_matches({ null => '' }, { null => { '$type' => 'null' } }), "false values don't match as nulls");

# let's perform some complex queries
ok(doc_matches({
	integer => 12,
	date => '2011-06-07T14:30:00+03:00',
	things => ['ball', 'bull', 'shit'],
}, {
	integer => { '$gte' => 5, '$lte' => 12 },
	date => { '$type' => 'date', '$lt' => '2020-06-07T14:30:00+03:00' },
	things => { '$all' => ['shit'] },
}), 'complex #1 okay');
ok(doc_matches({
	and => -12.5,
	now => 'now',
	for => { needs_more => 'cowbell' },
	something => [ { one => 1 }, { two => 2 } ],
	name => 'Ido Perlmuter',
}, {
	and => { '$type' => 'float', '$lte' => 0 },
	now => { '$exists' => 1 },
	then => { '$exists' => 0 },
	for => { '$type' => 'hash', '$size' => 1 },
	name => 'Ido Perlmuter',
}), 'complex #2 okay');
ok(doc_matches({
	type => 'blog',
	name => 'vlog',
	tags => [qw/sex drugs rocknroll/],
	members => {
		ido => 'admin',
		moses => 'leader',
		jesus => 'savior',
		misus => 'wife',
	},
	score => 8.5,
}, {
	type => { '$in' => [qw/newspaper blog forum lie/] },
	name => { '$nin' => [qw/something inappropriate and stuff/] },
	members => { '$type' => 'hash' },
	score => { '$gte' => 7, '$mod' => [5, 3] },
}), 'complex #3 okay');

# let's try some $or queries
ok(doc_matches({ title => 'Freaks and Geeks' }, { '$or' => [ { title => 'Freaks and Geeks' }, { title => 'Undeclared' } ] }), '$or #1 works');
ok(!doc_matches({ title => 'How I Met Your Mother' }, { '$or' => [ { title => 'Freaks and Geeks' }, { title => 'Undeclared' } ] }), '$or #2 works');
ok(doc_matches({ one => 1 }, { '$or' => [ { one => { '$exists' => 1 } }, { two => { '$exists' => 1 } } ] }), '$or #3 works');
ok(doc_matches({ two => 2 }, { '$or' => [ { one => { '$exists' => 1 } }, { two => { '$exists' => 1 } } ] }), '$or #4 works');
ok(!doc_matches({ three => 3 }, { '$or' => [ { one => { '$exists' => 1 } }, { two => { '$exists' => 1 } } ] }), '$or #5 works');
ok(doc_matches({
	year => 2010,
	genre => 'comedy',
}, {
	year => { '$gt' => 2000 },
	'$or' => [
		{ genre => 'comedy' },
		{ genre => 'drama' },
	],
}), 'or #6 works');
ok(!doc_matches({
	year => 2010,
	genre => 'mystery',
}, {
	year => { '$gt' => 2000 },
	'$or' => [
		{ genre => 'comedy' },
		{ genre => 'drama' },
	],
}), 'or #7 works');
ok(!doc_matches({
	year => 1990,
	genre => 'comedy',
}, {
	year => { '$gt' => 2000 },
	'$or' => [
		{ genre => 'comedy' },
		{ genre => 'drama' },
	],
}), 'or #8 works');
ok(doc_matches({
	key_one => 0,
	key_two => 0,
	key_three => 'Something',
}, {
	'$or' => [
		{ key_three => 'Cool' },
		{
			key_one => 0,
			key_three => { '$in' => ['Someone', 'Something'] },
		},
	],
}), 'or #9 works');

# let's try some $and queries
ok(doc_matches({ a => 5, b => 5 }, { 
	'$and' => [
		{ '$or' => [{ a => 5 }, { b => 10 }] }, 
		{ '$or' => [{ a => 1 }, { b => 5  }] }
	]
}), 'checking $and query #1');

ok(doc_matches({ a => 1, b => 2 }, { 
	'$and' => [
		{ a => { '$lt' => 2 } },
		{ b => { '$gt' => 1 } }
	]
}), 'checking $and query #2');

ok(!doc_matches({ a => 1, b => 2 }, {
	'$and' => [
		{ a => 1 },
		{ b => 1 }
	]
}), 'checking $and query #3');

# let's try some functions
ok(
	doc_matches(
		{ one => 1, two => 2, three => 3 },
		{ min => 1 },
		[ min => { '$min' => ['one', 'two', 'three'] } ]
	),
	'$min works with simple equality'
);

ok(
	doc_matches(
		{ one => 1, two => 2, three => 3 },
		{ max => 3 },
		[ max => { '$max' => ['one', 'two', 'three'] } ]
	),
	'$max works with simple equality'
);

ok(
	doc_matches(
		{ one => 1, two => 2, three => 3 },
		{ max => { '$gt' => 2, '$lt' => 4 } },
		[ max => { '$max' => ['one', 'two', 'three'] } ]
	),
	'$max works with complex ranging'
);

ok(
	doc_matches(
		{ some_value => -5.94 },
		{ 'abs(some_value)' => { '$gte' => 5, '$lte' => 6 } },
		[ 'abs(some_value)' => { '$abs' => 'some_value' } ]
	),
	'$abs works'
);

ok(
	doc_matches(
		{ two => 2, four => 4, eight => 8 },
		{ product => 64 },
		[ product => { '$product' => [qw/two four eight/] } ]
	),
	'product() works'
);

ok(
	doc_matches(
		{ four => 4, two => 2 },
		{ div => 2 },
		[ div => { '$div' => ['four', 'two'] } ]
	),
	'div() works'
);

ok(
	doc_matches(
		{ four => 4, zero => 0, three => 3 },
		{ div => 0 },
		[ div => { '$div' => ['four', 'zero', 'three'] } ]
	),
	'div() by zero returns zero'
);

ok(
	doc_matches(
		{ four => 4, zero => 0, three => 3 },
		{ sum => 7 },
		[ sum => { '$sum' => ['four', 'zero', 'three'] } ]
	),
	'sum() works'
);

ok(
	doc_matches(
		{ four => 4, grand => 1000, mfour => -4 },
		{ diff => -992 },
		[ diff => { '$diff' => [qw/four grand mfour/] } ]
	),
	'diff() works'
);

# let's try function nesting
ok(
	doc_matches(
		{ one => 1, two => 2, three => 3 },
		{ sum => 4 },
		[ min => { '$min' => ['one', 'two', 'three'] },
		  max => { '$max' => ['one', 'two', 'three'] },
		  sum => { '$sum' => ['min', 'max'] } ]
	),
	'nested function #1 works'
);

ok(
	doc_matches(
		{ ten => 10, nine => 9, three => 3, four => 4 },
		{ abs => 20 },
		[ diff1 => { '$diff' => ['three', 'four'] },
		  diff2 => { '$diff' => ['diff1', 'ten', 'nine'] },
		  abs => { '$abs' => 'diff2' } ]
	),
	'nested function #2 works'
);

# what if we're running a function on non-existing values?
ok(
	!doc_matches(
		{},
		{ abs => 20 },
		[ abs => { '$abs' => 'something' } ]
	),
	'abs() on a non-existing value does not croak'
);

ok(
	doc_matches(
		{},
		{ min => { '$type' => 'null' } },
		[ min => { '$min' => [qw/one two three/] } ]
	),
	'min() on non-existing values does not croak but returns null'
);

# let's check the dot notation
ok(
	doc_matches(
		{ some => { thing => { very => { deep => 3 } }, other_thing => 1 } },
		{ 'some.thing.very.deep' => { '$gt' => 2 } }
	),
	'dot notation works on simple nested hash'
);

ok(
	doc_matches(
		{ some => { thing => 1 } },
		{ 'some.thing.very.deep' => { '$exists' => 0 } }
	),
	'dot notation doesn\'t die on long nesting that doesn\'t exist'
);

ok(
	doc_matches(
		{ some => { thing => [1, 2, 3] } },
		{ 'some.thing' => { '$size' => 3 } }
	),
	'dot notation gets correct size of array'
);

ok(
	!doc_matches(
		{ hey => { man => 1 } },
		{ 'hey.man.what' => 3 }
	),
	'dot notation doesn\'t die on bad nesting'
);

# let's try the dot notation with arrays
ok(
	doc_matches(
		{ some => { thing => [5, 0, 5] } },
		{ 'some.thing.1' => { '$lt' => 1 } }
	),
	'dot notation works with arrays #1'
);

ok(
	!doc_matches(
		{ some => { thing => [5, 0, 5] } },
		{ 'some.thing.1' => { '$gt' => 1 } }
	),
	'dot notation works with arrays #2'
);

ok(
	doc_matches(
		{ some => { thing => [ { zero => 0 }, { one => 1 }, { two => 2 } ] } },
		{ 'some.thing.1.one' => { '$exists' => 1 } }
	),
	'dot notation works with arrays of hashes #1'
);

ok(
	!doc_matches(
		{ some => { thing => [ { zero => 0 }, { one => 1 }, { two => 2 } ] } },
		{ 'some.thing.1.zero' => { '$exists' => 1 } }
	),
	'dot notation works with arrays of hashes #2'
);

ok(
	doc_matches(
		{ one => { value => 3.25 }, two => { value => 5.75 } },
		{ 'two.value' => { '$ne' => 3.25 }, 'one.value' => { '$type' => 'float' }, 'one.missing' => { '$exists' => 0 } }
	),
	'dot notation works with multiple complex queries'
);

ok(
	doc_matches(
		{ one => { value => 3.25 }, two => { value => 5.75 } },
		{ '$or' => [ { 'one.value' => { '$gt' => 4 } }, { 'two.value' => { '$gt' => 4 } } ] }
	),
	'dot notation works with $or queries #1'
);

ok(
	!doc_matches(
		{ one => { value => 3.25 }, two => { value => 5.75 } },
		{ '$or' => [ { 'one.value' => { '$gt' => 4 } }, { 'two.value' => { '$gt' => 6 } } ] }
	),
	'dot notation works with $or queries #2'
);

ok(
	doc_matches(
		{ one => { value => -3.5 } },
		{ abs => 3.5 },
		[ abs => { '$abs' => 'one.value' } ]
	),
	'dot notation works with simple abs()'
);

ok(
	doc_matches(
		{ one => { value => 3.25 }, two => { value => 5.75 } },
		{ max => 5.75 },
		[ max => { '$max' => ['one.value', 'two.value'] } ]
	),
	'dot notation works with max() #1'
);

ok(
	!doc_matches(
		{ one => { value => 3.25 }, two => { value => 5.75 } },
		{ max => 3.25 },
		[ max => { '$max' => ['one.value', 'two.value'] } ]
	),
	'dot notation works with max() #2'
);

ok(
	doc_matches(
		{ one => { array => [{ value => 1 }] }, two => { array => [{ value => 2 }] } },
		{ min => { '$gte' => 1, '$lt' => 2 } },
		[ min => { '$min' => ['one.array.0.value', 'two.array.0.value'] } ]
	),
	'dot notation works with functions and arrays #1'
);

ok(
	!doc_matches(
		{ one => { array => [{ value => 1 }] }, two => { array => [{ value => 2 }] } },
		{ min => { '$gt' => 1, '$lt' => 2 } },
		[ min => { '$min' => ['one.array.0.value', 'two.array.0.value'] } ]
	),
	'dot notation works with functions and arrays #2'
);

ok(
	doc_matches(
		{
			some => { thing => { very => { deep => 3 } } },
			array => { of => { hashes => [  { one => 1 }, { two => 2 } ] } }
		},
		{
			'array.of.hashes' => { '$type' => 'array' },
			'array.of.hashes.0' => { '$type' => 'hash' }
		}
	),
	'dot notation returns proper types'
);

ok(
	doc_matches(
		{
			numbers => {
				one => 35,
				two => -65,
				three => 100
			},
			array_of_numbers => [80, 90]
		},
		{ max => { '$gt' => 90 } },
		[ max => { '$max' => ['array_of_numbers.1', 'numbers.three'] } ]
	),
	'max with dot notation from different fields works'
);

done_testing();