#!/usr/bin/env perl
# vim: set ts=8 sts=2 sw=2 tw=100 et ft=perl :
use strict;
use warnings;
use 5.020;
no autovivification warn => qw(fetch store exists delete);
use if "$]" >= 5.022, experimental => 're_strict';
no if "$]" >= 5.031009, feature => 'indirect';
no if "$]" >= 5.033001, feature => 'multidimensional';
no if "$]" >= 5.033006, feature => 'bareword_filehandles';
no if "$]" >= 5.041009, feature => 'smartmatch';
no feature 'switch';
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8
use Path::Tiny;
use Mojo::UserAgent;
use Digest::MD5 'md5_hex';
use Test::File::ShareDir -share => { -dist => { 'OpenAPI-Modern' => 'share' } };
use lib 'lib';
use JSON::Schema::Modern;
use JSON::Schema::Modern::Document::OpenAPI;

# see https://spec.openapis.org/#openapi-specification-schemas for the latest links

# TODO: do not store the exact URIs of these files; instead store a mapping of URI regexes to the
# filenames in which we will store them.
# Also carefully arrange the order of them so dependencies are loaded first.

my %files = (
  # metaschema for json schemas contained within openapi documents:
  # standard json schema (presently draft2020-12) + OAD vocabulary
  'oas/dialect/base.schema.json' => JSON::Schema::Modern::Document::OpenAPI::DEFAULT_DIALECT,

  # OAD vocabulary definition
  'oas/meta/base.schema.json' => JSON::Schema::Modern::Document::OpenAPI::OAS_VOCABULARY,

  # openapi document schema that forces the use of the json schema dialect (no $schema overrides
  # permitted)
  'oas/schema-base.json' => JSON::Schema::Modern::Document::OpenAPI::DEFAULT_BASE_METASCHEMA,

  # the main openapi document schema, with permissive (unvalidated) json schemas
  'oas/schema.json' => JSON::Schema::Modern::Document::OpenAPI::DEFAULT_METASCHEMA,

  'oas/LICENSE' => 'https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/LICENSE',
);

my $web_url = 'https://spec.openapis.org/';

my $ua = Mojo::UserAgent->new(max_redirects => 3);
say "# fetching $web_url" if $ENV{DEBUG};
my $res = $ua->get($web_url)->result;
die "Failed to fetch $web_url", $res->code, " ", $res->message if $res->is_error;

my %new_schemas = ();  # keys are existing OAS schema URIs, defined above in %files

# check the website and find all files that are newer than what we've got
foreach my $e ($res->dom->find('a[href]')->each) {
  my $link = $e->{href};
  next if $link !~ m{^/oas/3.1/[a-z-]+/\d{4}-\d{2}-\d{2}$};

  $link = Mojo::URL->new($web_url)->path($link)->to_string; # normalize slashes

  if (my ($existing) = grep !/github/ && $link =~ m{^\Q${\substr($_, 0, -10)}\E}, values %files) {
    $new_schemas{$existing} //= [];
    push $new_schemas{$existing}->@*, $link if $existing lt $link;
  }
}

# identify outdated schema files, and find all references to them in the repository
if (my @outdated_schemas = grep $new_schemas{$_}->@*, sort keys %new_schemas) {
  warn join("\n", 'these outdated files should be updated (fix the hash in update-schemas and re-run):',
    map +("$_ -> ".join(', ', sort $new_schemas{$_}->@*)), @outdated_schemas), "\n\n";

  my $re = join('|', map "\Q$_\E", @outdated_schemas);
  my @outdated_files = grep !exists $files{s!share/!!r} && path($_)->slurp_utf8 =~ $re, split /\n/, `git ls-files`;
  warn join("\n", 'all other files containing references to outdated files:', @outdated_files), "\n\n";

  warn join("\n", 'run this repeatedly in order to find bad references:',
    'ack -l \''.$re.'\' `git ls-files`'), "\n\n";
}


my $json_decoder = JSON::Schema::Modern::_JSON_BACKEND()->new->utf8(1);
my $js = JSON::Schema::Modern->new(strict => 1, validate_formats => 1);
my %checksums;

# download fresh copies of our files, validate against their schemas and against our code
foreach my $target (sort keys %files) {
  my $uri = $files{$target};

  say "# fetching   $uri -> share/$target" if $ENV{DEBUG};
  my $res = $ua->get($uri)->result;
  die "Failed to fetch $uri", $res->code, " ", $res->message if $res->is_error;

  $target = path('share', $target);
  $target->parent->mkpath;
  $target->spew_raw(my $content = $res->body);
  $checksums{$target} = md5_hex($content);

  next if $target->basename eq 'LICENSE';

  # perform a simple validation, which should use a metaschema that is already preloaded into the
  # JSON::Schema::Modern instance
  my $schema = $json_decoder->decode($content);
  say '# validating ', $schema->{'$id'}, ' -> ', $target if $ENV{DEBUG};
  my $result = $js->validate_schema($schema, { strict => 1 });
  die $result->dump if not $result->valid;
}

# all files must be updated before any of them can be loaded as documents, since they depend on each
# other via the '$schema' and '$vocabulary' keywords

foreach my $target (sort keys %files) {
  $target = path('share', $target);
  next if $target->basename eq 'LICENSE';

  my $schema = $json_decoder->decode($target->slurp_raw);
  say '# loading    ', $schema->{'$id'}, ' -> ', $target if $ENV{DEBUG};

  # Note that if these are new files, then this type of validate won't work...
  # because the files won't be in the resource index. But we can fix this by carefully rearranging
  # the order in which we load the files, and can sequentially add them to the evaluator's resource
  # index.

  my $result = JSON::Schema::Modern::Document->validate(schema => $schema);
  die $result->dump if not $result->valid;

  die 'for uri ', $schema->{'$id'}, ', mismatch between jsonSchemaDialect "',
      $schema->{properties}{jsonSchemaDialect}{default},
      '" and assumed default "',
      JSON::Schema::Modern::Document::OpenAPI->DEFAULT_DIALECT, '"'
    if exists((($schema->{properties}//{})->{jsonSchemaDialect}//{})->{default})
      and $schema->{properties}{jsonSchemaDialect}{default}
        ne JSON::Schema::Modern::Document::OpenAPI->DEFAULT_DIALECT;
}

# compute checksums and record them in the test
path('t/checksums.t')->edit_raw(sub {
  m/^__DATA__$/mg;
  $_ = substr($_, 0, pos()+1).join("\n", map $_.' '.$checksums{$_}, sort keys %checksums)."\n";
});
