use Mojo::Base -strict;

BEGIN { $ENV{MOJO_REACTOR} = 'Mojo::Reactor::Poll' }

use Test::More;

plan skip_all => 'set TEST_MORBO to enable this test (developer only!)' unless $ENV{TEST_MORBO} || $ENV{TEST_ALL};

use Mojo::File qw(curfile);
use lib curfile->sibling('lib')->to_string;

use IO::Socket::INET ();
use Mojo::File       qw(tempdir);
use Mojo::IOLoop::Server;
use Mojo::Server::Morbo::Backend;
use Mojo::Server::Daemon;
use Mojo::Server::Morbo;
use Mojo::UserAgent;
use Socket      qw(SO_REUSEPORT SOL_SOCKET);
use Time::HiRes qw(sleep);

# Start
my $dir    = tempdir;
my $script = $dir->child('myapp.pl');
my $subdir = $dir->child('test', 'stuff')->make_path;
my $morbo  = Mojo::Server::Morbo->new();
$morbo->backend->watch([$subdir, $script]);
is_deeply $morbo->backend->modified_files, [], 'no files have changed';
my $started = $dir->child('started1.txt');
$script->spew(<<EOF);
use Mojolicious::Lite;
use Mojo::File qw(path);
use Mojo::IOLoop;

app->log->level('fatal');

Mojo::IOLoop->next_tick(sub { path('$started')->touch });

get '/hello' => {text => 'Hello Morbo!'};

app->start;
EOF
my $port   = Mojo::IOLoop::Server->generate_port;
my $prefix = curfile->dirname->dirname->sibling('script');
my $pid    = open my $server, '-|', $^X, "$prefix/morbo", '-l', "http://127.0.0.1:$port", $script;
sleep 0.1 while !_port($port);
my $ua = Mojo::UserAgent->new;

subtest 'Basics' => sub {
  my $tx = $ua->get("http://127.0.0.1:$port/hello");
  ok $tx->is_finished, 'transaction is finished';
  is $tx->res->code, 200,            'right status';
  is $tx->res->body, 'Hello Morbo!', 'right content';

  $tx = $ua->get("http://127.0.0.1:$port/hello");
  ok $tx->is_finished, 'transaction is finished';
  is $tx->res->code, 200,            'right status';
  is $tx->res->body, 'Hello Morbo!', 'right content';
};

subtest 'Update script without changing size' => sub {
  my ($size, $mtime) = (stat $script)[7, 9];
  $started = $started->sibling('started2.txt');
  $script->spew(<<EOF);
use Mojolicious::Lite;
use Mojo::File qw(path);
use Mojo::IOLoop;

app->log->level('fatal');

Mojo::IOLoop->next_tick(sub { path('$started')->touch });

get '/hello' => {text => 'Hello World!'};

app->start;
EOF
  is_deeply $morbo->backend->modified_files, [$script], 'file has changed';
  ok((stat $script)[9] > $mtime, 'modify time has changed');
  is((stat $script)[7], $size, 'still equal size');
  sleep 0.1 until -f $started;

  # Application has been reloaded
  my $tx = $ua->get("http://127.0.0.1:$port/hello");
  ok $tx->is_finished, 'transaction is finished';
  is $tx->res->code, 200,            'right status';
  is $tx->res->body, 'Hello World!', 'right content';

  # Same result
  $tx = $ua->get("http://127.0.0.1:$port/hello");
  ok $tx->is_finished, 'transaction is finished';
  is $tx->res->code, 200,            'right status';
  is $tx->res->body, 'Hello World!', 'right content';
};

subtest 'Update script without changing mtime' => sub {
  my ($size, $mtime) = (stat $script)[7, 9];
  is_deeply $morbo->backend->modified_files, [], 'no files have changed';
  $started = $started->sibling('started3.txt');
  $script->spew(<<"EOF");
use Mojolicious::Lite;
use Mojo::File qw(path);
use Mojo::IOLoop;

app->log->level('fatal');

Mojo::IOLoop->next_tick(sub { path('$started')->touch });

my \$message = 'Failed!';
hook before_server_start => sub { \$message = 'Hello!' };

get '/hello' => sub { shift->render(text => \$message) };

app->start;
EOF
  utime $mtime, $mtime, $script;
  is_deeply $morbo->backend->modified_files, [$script], 'file has changed';
  ok((stat $script)[9] == $mtime, 'modify time has not changed');
  isnt((stat $script)[7], $size, 'size has changed');
  sleep 0.1 until -f $started;

  # Application has been reloaded again
  my $tx = $ua->get("http://127.0.0.1:$port/hello");
  ok $tx->is_finished, 'transaction is finished';
  is $tx->res->code, 200,      'right status';
  is $tx->res->body, 'Hello!', 'right content';

  # Same result
  $tx = $ua->get("http://127.0.0.1:$port/hello");
  ok $tx->is_finished, 'transaction is finished';
  is $tx->res->code, 200,      'right status';
  is $tx->res->body, 'Hello!', 'right content';
};

subtest 'Watch for deleted files' => sub {
  my $morbo = Mojo::Server::Morbo->new;
  $morbo->backend->watch([$dir]);
  $morbo->backend->modified_files;
  my $tmp_file = $dir->child('tmp_file.txt');
  $tmp_file->spew("some data");
  is_deeply $morbo->backend->modified_files, [$tmp_file], 'file was created';
  is_deeply $morbo->backend->modified_files, [],          'no files changed';
  unlink $tmp_file;
  is_deeply $morbo->backend->modified_files, [$tmp_file], 'file was deleted';
  is_deeply $morbo->backend->modified_files, [],          'no files changed';
};

subtest 'New file(s)' => sub {
  is_deeply $morbo->backend->modified_files, [], 'directory has not changed';
  my @new = map { $subdir->child("$_.txt") } qw/test testing/;
  $_->spew('whatever') for @new;
  is_deeply $morbo->backend->modified_files, \@new, 'two files have changed';
  $subdir->child('.hidden.txt')->spew('whatever');
  is_deeply $morbo->backend->modified_files, [], 'directory has not changed again';
};

subtest 'Broken symlink' => sub {
  plan skip_all => 'Symlink support required!' unless eval { symlink '', ''; 1 };
  my $missing = $subdir->child('missing.txt');
  my $broken  = $subdir->child('broken.txt');
  symlink $missing, $broken;
  ok -l $broken,  'symlink created';
  ok !-f $broken, 'symlink target does not exist';
  my $warned;
  local $SIG{__WARN__} = sub { $warned++ };
  is_deeply $morbo->backend->modified_files, [], 'directory has not changed';
  ok !$warned, 'no warnings';
};

# Stop
kill 'INT', $pid;
sleep 0.1 while _port($port);

subtest 'Custom backend' => sub {
  local $ENV{MOJO_MORBO_BACKEND} = 'TestBackend';
  local $ENV{MOJO_MORBO_TIMEOUT} = 2;
  my $test_morbo = Mojo::Server::Morbo->new;
  isa_ok $test_morbo->backend, 'Mojo::Server::Morbo::Backend::TestBackend', 'right backend';
  is_deeply $test_morbo->backend->modified_files, ['always_changed'], 'always changes';
  is $test_morbo->backend->watch_timeout, 2, 'right timeout';
};

subtest 'SO_REUSEPORT' => sub {
  plan skip_all => 'SO_REUSEPORT support required!' unless eval { _reuse_port() };
  my $port   = Mojo::IOLoop::Server->generate_port;
  my $daemon = Mojo::Server::Daemon->new(listen => ["http://127.0.0.1:$port"], silent => 1)->start;
  ok !$daemon->ioloop->acceptor($daemon->acceptors->[0])->handle->getsockopt(SOL_SOCKET, SO_REUSEPORT),
    'no SO_REUSEPORT socket option';
  $daemon = Mojo::Server::Daemon->new(listen => ["http://127.0.0.1:$port?reuse=1"], silent => 1);
  $daemon->start;
  ok $daemon->ioloop->acceptor($daemon->acceptors->[0])->handle->getsockopt(SOL_SOCKET, SO_REUSEPORT),
    'SO_REUSEPORT socket option';
};

subtest 'Abstract methods' => sub {
  eval { Mojo::Server::Morbo::Backend->modified_files };
  like $@, qr/Method "modified_files" not implemented by subclass/, 'right error';
};

sub _port { IO::Socket::INET->new(PeerAddr => '127.0.0.1', PeerPort => shift) }

sub _reuse_port { IO::Socket::INET->new(Listen => 1, LocalPort => Mojo::IOLoop::Server->generate_port, ReusePort => 1) }

done_testing();
