use v5.36;
use experimental 'builtin', 'for_list';
use builtin 'true', 'false';
use Test::More;
use Tie::Array;
use Tie::Hash;
use FU::Util 'json_format';
use Config;


sub MyToJSON::TO_JSON { [scalar @_, ref $_[0], ${$_[0]}] }
sub MyToJSONSelf::TO_JSON { $_[0] }

my @tests = (
    undef, 'null',
    true, 'true',
    false, 'false',
    (map +($_, $_),
        -1000..1000,
        12345, 123456, 1234567,
        98765, 987654, 9876543,
        -12345, -123456, -1234567,
        -98765, -987654, -9876543,
        -9223372036854775808,
        9223372036854775807,
        18446744073709551615,
    ),
    0.1, '0.1',
    0.000123, '0.000123',
    -1e100, '-1e+100',

    do { use utf8; (
        "\x01é\r\n\x1f💩", '"\u0001é\r\n\u001f💩"',
    )},

    "\x011\r\n\x8c", "\"\\u00011\\r\\n\x8c\"",
    "\xff\xff", "\"\xff\xff\"",
    "\x{1f4a9}", do { use utf8; '"💩"' },

    [], '[]',
    [0,1], '[0,1]',
    [true,'hi',-0.123, [undef]], '[true,"hi",-0.123,[null]]',
    do { tie my @a, 'Tie::StdArray'; @a = (1,2); \@a }, '[1,2]',

    {}, '{}',
    {'a',1}, '{"a":1}',
    do { tie my %h, 'Tie::StdHash'; %h = ('b',1); \%h }, '{"b":1}',
    do { tie my %h, 'MyOrderedHash', one => 1, two => undef, three => []; \%h }, '{"one":1,"two":null,"three":[]}',

    do { my $o = [true]; bless \$o, 'MyToJSON' }, '[1,"MyToJSON",[true]]',
    do { my $x = [true]; my $o = [bless \$x, 'MyToJSON']; bless \$o, 'MyToJSON' }, '[1,"MyToJSON",[[1,"MyToJSON",[true]]]]',

    # from http://e-choroba.eu/18-yapc
    $$, $$,
    ''.$$, '"'.$$.'"',
    do { my $x = 12; utf8::decode($x); $x }, '"12"',
    do { no warnings 'numeric'; my $x = '19a'; $x += 0; $x }, '19',
    $Config{uselongdouble} ? () : ( 1844674407370955161 / 10, '1.84467440737096e+17' ),
);

my @errors = (
    \2, qr/unable to format reference/,
    *STDOUT, qr/unable to format unknown value/,
    'NaN'+0, qr/unable to format floating point NaN or Inf as JSON/,
    'Inf'+0, qr/unable to format floating point NaN or Inf as JSON/,
    do { my $o = {}; bless $o, 'FU::Whatever' }, qr/unable to format 'FU::Whatever' object as JSON/,
    do { my $o = {}; bless $o, 'MyToJSONSelf' }, qr/MyToJSONSelf::TO_JSON method returned same object as was passed instead of a new one/,
);


for my($in, $exp) (@tests) {
    my $out = json_format $in;
    is $out, $exp;
    ok utf8::is_utf8($out);

    $out = json_format $in, utf8 => 1;
    utf8::encode(my $uexp = $exp);
    is $out, $uexp;
    ok !utf8::is_utf8($out);
}

for my ($in, $exp) (@errors) {
    eval { json_format $in };
    like $@, $exp;
}


is json_format({qw/a 1 b 2 c 3 d 4 d1 5 d11 6/, do { use utf8; qw/ü 7 月 8 💩 9/ }}, canonical => 1),
   do { use utf8; '{"a":"1","b":"2","c":"3","d":"4","d1":"5","d11":"6","ü":"7","月":"8","💩":"9"}' };

is json_format(
      { a => [], b => {}, c => { x => 1 }, d => { y => true, z => false }, e => [1,2,3] },
      canonical => 1, pretty => 1
    ), <<_;
{
   "a" : [],
   "b" : {},
   "c" : {
      "x" : 1
   },
   "d" : {
      "y" : true,
      "z" : false
   },
   "e" : [
      1,
      2,
      3
   ]
}
_

is json_format('<hello & world>', html_safe => 1), '"\u003chello \u0026 world\u003e"';

eval { json_format [[]], max_depth => 2 };
like $@, qr/max_depth exceeded while formatting JSON/;

eval { json_format 'hello world', max_size => 8 };
like $@, qr/maximum string length exceeded/;


# Test large strings to cover some buffer handling special cases.
for (2000..2100, 4000..4200, 8100..8200, 12200..12300, 16300..16400) {
    my $s = 'a'x$_;
    is json_format($s), "\"$s\"";
}

# 500 depth
{
    my $v = 1;
    $v = [$v] for (1..500);
    is json_format($v), '['x500 . 1 . ']'x500;
}
{
    my $v = 1;
    $v = {'',$v} for (1..500);
    is json_format($v), '{"":'x500 . 1 . '}'x500;
}


# http://e-choroba.eu/18-yapc slide 6

tie my $incs, 'MyIncrementer', 'Xa';
is json_format($incs), '"Xa"';
is json_format($incs), '"Xb"';
is json_format($incs), '"Xc"';

tie my $incu, 'MyIncrementer', 4;
is json_format($incu), 4;
is json_format($incu), 5;
is json_format($incu), 6;


done_testing;

package MyIncrementer;
use Tie::Scalar;
use parent -norequire => 'Tie::StdScalar';
sub TIESCALAR { my ($class, $val) = @_; bless \$val, $class }
sub FETCH { my $s = shift; $$s++ }


package MyOrderedHash;
sub TIEHASH { shift; bless [ [ map $_[$_*2], 0..$#_/2 ], +{@_}, 0 ], __PACKAGE__ };
sub FETCH { $_[0][1]{$_[1]} }
sub EXISTS { exists $_[0][1]{$_[1]} }
sub FIRSTKEY { $_[0][2] = 0; &NEXTKEY }
sub NEXTKEY { $_[0][0][ $_[0][2]++ ] }
sub SCALAR { scalar $_[0][0]->@* }
