use lib 'lib';
use lib 'specs/support';
use BDD::Behave;
use BloginTest;
use Blogin::Server;
use Blogin::Site;
use JSON::Fast;

describe 'the reload client script', {
  let(:script, { Blogin::Server::reload-script(version => 7, session => 'abc') });

  it 'opens a socket to the reload endpoint', {
    expect(script().contains('new WebSocket(') && script().contains('/__blogin-reload')).to.be-truthy;
  }

  it 'carries the build version the page was served at', {
    expect(script().contains('"version":7')).to.be-truthy;
  }

  it 'carries the server session the page was served by', {
    expect(script().contains('"session":"abc"')).to.be-truthy;
  }

  it 'reloads when the page it is running on changed', {
    expect(script().contains('location.reload()')).to.be-truthy;
  }

  it 'swaps a changed stylesheet in place rather than reloading', {
    expect(script().contains('link[rel="stylesheet"]')).to.be-truthy;
  }

  it 'reconnects after the socket closes so a restarted server recovers', {
    expect(script().contains('socket.onclose') && script().contains('setTimeout(connect')).to.be-truthy;
  }
}

describe 'injecting the reload client', {
  it 'inserts the script before the closing body tag', {
    my $out = Blogin::Server::inject-reload('<html><body>hi</body></html>', '<script>x</script>');
    expect($out).to.eq('<html><body>hi<script>x</script></body></html>');
  }

  it 'appends the script when there is no body tag', {
    expect(Blogin::Server::inject-reload('<p>hi</p>', '<script>x</script>')).to.eq('<p>hi</p><script>x</script>');
  }
}

describe 'the reload channel', {
  let(:channel, { Blogin::Server::ReloadChannel.new });

  it 'starts at version zero', {
    expect(channel().version).to.eq(0);
  }

  it 'has a session that identifies this server run', {
    expect(channel().session.chars).to.be-truthy;
  }

  it 'bumps the version on notify', {
    my $reload = channel();
    $reload.notify(['index.html']);

    expect($reload.version).to.eq(1);
  }

  it 'greets a new page with the current version', {
    my $reload = channel();
    $reload.notify(['index.html']);

    expect(from-json($reload.hello)<version>).to.eq(1);
  }

  it 'greets a new page with the session', {
    my $reload = channel();

    expect(from-json($reload.hello)<session>).to.eq($reload.session);
  }
}

describe 'a change pushed to connected pages', {
  sub pushed-for(@paths) {
    my $reload = Blogin::Server::ReloadChannel.new;
    my $seen   = Promise.new;

    $reload.messages.tap(-> $message { $seen.keep($message) });
    $reload.notify(@paths);

    from-json(await $seen);
  }

  it 'names the paths that changed', {
    expect(pushed-for(['posts/hello.html'])<paths>).to.eq(['posts/hello.html']);
  }

  it 'carries the version the change produced', {
    expect(pushed-for(['posts/hello.html'])<version>).to.eq(1);
  }

  it 'marks the message as a change', {
    expect(pushed-for(['posts/hello.html'])<type>).to.eq('change');
  }
}

describe 'the changed output paths of a build', {
  it 'reports each written file relative to the site root', {
    my $root = '/tmp/site'.IO;

    expect(Blogin::Server::changed-paths(['/tmp/site/posts/hello.html'.IO], $root)).to.eq(['posts/hello.html']);
  }

  it 'reports a path only once', {
    my $root = '/tmp/site'.IO;
    my @files = '/tmp/site/index.html'.IO, '/tmp/site/index.html'.IO;

    expect(Blogin::Server::changed-paths(@files, $root)).to.eq(['index.html']);
  }
}

describe 'the server console', {
  class Collector {
    has @.lines;

    method print(*@args) { @!lines.push(@args.join) }
    method say(*@args)   { @!lines.push(@args.join) }
  }

  let(:inner, { Collector.new });
  let(:err, { Blogin::Server::FilteredErr.new(inner => inner(), drop => Blogin::Server::SOCKET-NOISE) });

  it 'drops the notice a browser leaves when it closes its reload socket', {
    err().say('Cannot write to a closed socket');

    expect(inner().lines).to.eq([]);
  }

  it 'drops the notice a reset connection leaves', {
    err().say('connection reset by peer');

    expect(inner().lines).to.eq([]);
  }

  it 'passes a real error through', {
    err().say('something actually broke');

    expect(inner().lines).to.eq(['something actually broke']);
  }

  it 'filters what is printed without a newline too', {
    err().print('connection reset by peer');

    expect(inner().lines).to.eq([]);
  }

  it 'filters what is put', {
    err().put('connection reset by peer');

    expect(inner().lines).to.eq([]);
  }
}

describe 'the build options a preview runs with', {
  let(:options, {
    Blogin::Server::preview-options(%( minify => True, fingerprint => True, page-size => 10 ))
  });

  it 'never minifies, so served assets keep the names the page asked for', {
    expect(options()<minify>).to.be-falsy;
  }

  it 'never fingerprints, so a rebuild does not rename what an open page loaded', {
    expect(options()<fingerprint>).to.be-falsy;
  }

  it 'leaves every other build option alone', {
    expect(options()<page-size>).to.eq(10);
  }

  it 'leaves the configured options untouched', {
    my %config = minify => True, fingerprint => True;
    Blogin::Server::preview-options(%config);

    expect(%config<minify>).to.be-truthy;
  }
}

describe 'the served response', {
  my $BASIC = 'specs/fixtures/basic'.IO;

  let(:out, {
    my $dir = temp-dir('render-response');
    Blogin::Site::build(content => $BASIC.add('content'), out => $dir, layouts => $BASIC.add('layouts'), static => $BASIC.add('static'), home-section => 'posts');
    $dir
  });

  after-each { nuke(out()) }

  it 'injects the reload client into an html page when reload is on', {
    my %response = Blogin::Server::render-response('/posts/hello', out(), inject => True);
    expect(%response<body>.contains('/__blogin-reload')).to.be-truthy;
  }

  it 'does not inject the client when reload is off', {
    my %response = Blogin::Server::render-response('/posts/hello', out(), inject => False);
    expect(%response<body>.decode.contains('__blogin-reload')).to.be-falsy;
  }

  it 'serves a non-html asset without injection', {
    my %response = Blogin::Server::render-response('/style.css', out(), inject => True);
    expect(%response<body> ~~ Blob && !%response<body>.decode.contains('__blogin-reload')).to.be-truthy;
  }

  it 'returns a 404 for a missing path', {
    my %response = Blogin::Server::render-response('/nope', out(), inject => True);
    expect(%response<status>).to.eq(404);
  }
}

describe 'a rebuild that changed output', {
  let(:channel, { Blogin::Server::ReloadChannel.new });

  let(:result, {
    class { has @.write-log }.new(write-log => ['/tmp/site/index.html'.IO])
  });

  it 'runs the rebuild', {
    my $ran = False;
    Blogin::Server::rebuild-and-notify(channel(), { $ran = True; result() }, '/tmp/site'.IO);

    expect($ran).to.be-truthy;
  }

  it 'returns the changed paths', {
    expect(Blogin::Server::rebuild-and-notify(channel(), { result() }, '/tmp/site'.IO)).to.eq(['index.html']);
  }

  it 'bumps the version so connected pages hear about it', {
    my $reload = channel();
    Blogin::Server::rebuild-and-notify($reload, { result() }, '/tmp/site'.IO);

    expect($reload.version).to.eq(1);
  }
}

describe 'a rebuild that changed nothing', {
  let(:channel, { Blogin::Server::ReloadChannel.new });
  let(:empty, { class { has @.write-log }.new(write-log => []) });

  it 'leaves the version alone so no page reloads', {
    my $reload = channel();
    Blogin::Server::rebuild-and-notify($reload, { empty() }, '/tmp/site'.IO);

    expect($reload.version).to.eq(0);
  }

  it 'reports no changed paths', {
    expect(Blogin::Server::rebuild-and-notify(channel(), { empty() }, '/tmp/site'.IO)).to.eq([]);
  }
}

describe 'a rebuild that failed', {
  it 'does not bump the version', {
    my $reload = Blogin::Server::ReloadChannel.new;

    try Blogin::Server::rebuild-and-notify($reload, { die 'boom' }, '/tmp/site'.IO);

    expect($reload.version).to.eq(0);
  }
}
