# vim: set ft=perl ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use 5.020;
use stable 0.031 'postderef';
use experimental 'signatures';
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 builtin::compat 'load_module';
use lib 't/lib';
use Helper;

subtest 'draft7' => sub {
  like(
    dies {
      JSON::Schema::Modern->new(collect_annotations => 1, specification_version => 'draft7');
    },
    qr/collect_annotations cannot be used with specification_version draft7/,
    'user cannot enable annotations for draft7',
  );

  cmp_result(
    JSON::Schema::Modern->new(specification_version => 'draft7')
      ->evaluate(
        1,
        {
          title => 'draft7 title',
          allOf => [ { '$ref' => '#/definitions/draft2020-12-schema' } ],
          definitions => {
            'draft2020-12-schema' => {
              '$id' => 'my_draft2020-12_schema',
              '$schema' => 'https://json-schema.org/draft/2020-12/schema',
              title => 'draft2020-12 title'
            },
          },
        },
        { collect_annotations => 1 },
      )->TO_JSON,
    {
      valid => true,
      annotations => [
        {
          instanceLocation => '',
          keywordLocation => '/allOf/0/$ref/title',
          absoluteKeywordLocation => 'my_draft2020-12_schema#/title',
          annotation => 'draft2020-12 title',
        },
      ],
    },
    'annotation collection can be enabled even when defaulting to draft7, but they are still only collected for later drafts',
  );
};

my $js = JSON::Schema::Modern->new(collect_annotations => 1, short_circuit => 0);

my $initial_state = {
  depth => 0,
  short_circuit => 0,
  collect_annotations => 1<<8,
  initial_schema_uri => Mojo::URL->new,
  data_path => '',
  keyword_path => '',
  traversed_keyword_path => '',
  specification_version => 'draft2019-09',
  vocabularies => [
    (map load_module($_),
      map 'JSON::Schema::Modern::Vocabulary::'.$_, qw(Applicator Validation MetaData)),
  ],
  evaluator => $js,
};

subtest 'allOf' => sub {
  my $state = {
    %$initial_state,
    keyword => 'allOf',
    annotations => [],
    errors => [],
  };

  my $fail_schema = {
    allOf => [
      false,                        # fails; creates errors
      { title => 'allOf title' },   # passes; creates annotations
    ],
  };

  ok(
    !$state->{vocabularies}[0]->_eval_keyword_allOf(1, $fail_schema, $state),
    'evaluation of the allOf keyword fails',
  );

  cmp_result(
    $state,
    my $new_state = {
      %$state,
      initial_schema_uri => str(''),
      annotations => [
        superhashof({
          instance_location => '',
          keyword_location => '/allOf/1/title',
          annotation => 'allOf title',
        }),
      ],
      errors => [
        methods(TO_JSON => { instanceLocation => '', keywordLocation => '/allOf/0', error => 'subschema is false' }),
        methods(TO_JSON => { instanceLocation => '', keywordLocation => '/allOf', error => 'subschema 0 is not valid' }),
      ],
    },
    'failing allOf: state is correct after evaluating',
  );

  my $pass_schema = {
    allOf => [
      true,
      { title => 'allOf title' }, # passes; creates annotations
      true,
    ],
  };
  $state->{annotations} = [];
  $state->{errors} = [];

  ok(
    $state->{vocabularies}[0]->_eval_keyword_allOf(1, $pass_schema, $state),
    'evaluation of the allOf keyword succeeds',
  );

  cmp_result(
    $state,
    {
      %$new_state,
      annotations => [
        superhashof({
          instance_location => '',
          keyword_location => '/allOf/1/title',
          annotation => 'allOf title',
        }),
      ],
      errors => [],
    },
    'passing allOf: state is correct after evaluating',
  );

  cmp_result(
    $js->evaluate(1, $pass_schema, { collect_annotations => 0 })->TO_JSON,
    { valid => true },
    'annotation collection can be turned off in evaluate()',
  );

  ok($js->collect_annotations, '...but the value is still true on the object');

  {
    my $js = JSON::Schema::Modern->new;
    ok(!$js->collect_annotations, 'collect_annotations defaults to false');
    cmp_result(
      $js->evaluate(1, $pass_schema, { collect_annotations => 1 })->TO_JSON,
      {
        valid => true,
        annotations => [
          {
            instanceLocation => '',
            keywordLocation => '/allOf/1/title',
            annotation => 'allOf title',
          },
        ],
      },
      'annotation collection can be turned on in evaluate() also',
    );
  }
};

subtest 'oneOf' => sub {
  my $state = {
    %$initial_state,
    keyword => 'oneOf',
    annotations => [],
    errors => [],
  };

  my $fail_schema = {
    oneOf => [
      false,                        # fails; creates errors
      { title => 'oneOf title' },   # passes; creates annotations
      { title => 'oneOf title2' },  # passes; creates annotations
    ],
  };

  ok(
    !$state->{vocabularies}[0]->_eval_keyword_oneOf(1, $fail_schema, $state),
    'evaluation of the oneOf keyword fails',
  );

  cmp_result(
    $state,
    my $new_state = {
      %$state,
      initial_schema_uri => str(''),
      annotations => [
        superhashof({
          instance_location => '',
          keyword_location => '/oneOf/1/title',
          annotation => 'oneOf title',
        }),
        superhashof({
          instance_location => '',
          keyword_location => '/oneOf/2/title',
          annotation => 'oneOf title2',
        }),
      ],
      errors => [
        methods(TO_JSON => { instanceLocation => '', keywordLocation => '/oneOf', error => 'multiple subschemas are valid: 1, 2' }),
      ],
    },
    'failing oneOf: state is correct after evaluating',
  );

  my $pass_schema = {
    oneOf => [
      false,
      { title => 'oneOf title' },  # passes; creates annotations
      false,
    ],
  };

  $state->{annotations} = [];
  $state->{errors} = [];

  ok(
    $state->{vocabularies}[0]->_eval_keyword_oneOf(1, $pass_schema, $state),
    'evaluation of the oneOf keyword succeeds',
  );

  cmp_result(
    $state,
    {
      %$new_state,
      annotations => [
        superhashof({
          instance_location => '',
          keyword_location => '/oneOf/1/title',
          annotation => 'oneOf title',
        }),
      ],
      errors => [],
    },
    'passing oneOf: state is correct after evaluating',
  );
};

subtest 'not' => sub {
  my $state = {
    %$initial_state,
    keyword => 'not',
    annotations => [],
    errors => [],
  };

  my $fail_schema = {
    not => { title => 'not title' },   # passes; skips annotations because nothing needs them
  };

  ok(
    !$state->{vocabularies}[0]->_eval_keyword_not(1, $fail_schema, $state),
    'evaluation of the not keyword fails',
  );

  cmp_result(
    $state,
    my $new_state = {
      %$state,
      initial_schema_uri => str(''),
      annotations => [],
      errors => [
        methods(TO_JSON => { instanceLocation => '', keywordLocation => '/not', error => 'subschema is valid' }),
      ],
    },
    'failing not: state is correct after evaluating',
  );


  $state = {
    %$initial_state,
    keyword => 'not',
    annotations => [],
    errors => [],
  };

  $fail_schema = {
    not => {
      properties => { foo => true },
      unevaluatedProperties => false,
    },   # passes; annotations are collected because unevaluated* needs them
  };

  ok(
    !$state->{vocabularies}[0]->_eval_keyword_not(1, $fail_schema, $state),
    'evaluation of the not keyword fails',
  );

  cmp_result(
    $state,
    $new_state = {
      %$state,
      initial_schema_uri => str(''),
      annotations => [],
      errors => [
        methods(TO_JSON => { instanceLocation => '', keywordLocation => '/not', error => 'subschema is valid' }),
      ],
    },
    'failing not: state is correct after evaluating (annotations will be ultimately discarded)',
  );


  my $pass_schema = {
    not => { not => { title => 'not title' } },
  };

  $state->{annotations} = [];
  $state->{errors} = [];

  ok(
    $state->{vocabularies}[0]->_eval_keyword_not(1, $pass_schema, $state),
    'evaluation of the not keyword succeeds',
  );

  cmp_result(
    $state,
    {
      %$new_state,
      annotations => [],
      errors => [],
    },
    'passing not: state is correct after evaluating',
  );

  cmp_result(
    $js->evaluate(
      { foo => 1 },
      {
        not => {
          not => {
            '$comment' => 'this subschema must still produce annotations internally, even though the "not" will ultimately discard them',
            anyOf => [
              true,
              { properties => { foo => true } },
            ],
            unevaluatedProperties => false,
          },
        },
      },
    )->TO_JSON,
    {
      valid => true,
    },
    'annotations are still collected inside a "not", otherwise the unevaluatedProperties would have returned false',
  );
};

subtest 'prefixItems' => sub {
  my $state = {
    %$initial_state,
    keyword => 'prefixItems',
    annotations => [],
    errors => [],
  };

  ok(
    $state->{vocabularies}[0]->_eval_keyword_prefixItems([], { prefixItems => [ true ] }, $state),
    'no items means that "prefixItems" succeeds',
  );

  cmp_result(
    $state,
    my $new_state = {
      %$state,
      initial_schema_uri => str(''),
      annotations => [],
      errors => [],
    },
    'no items: no annotation is produced by prefixItems',
  );

  $state = {
    %$initial_state,
    keyword => 'prefixItems',
    annotations => [],
    errors => [],
  };

  ok(
    $state->{vocabularies}[0]->_eval_keyword_prefixItems([ 1 ], { prefixItems => [ true ] }, $state),
    'one item',
  );

  cmp_result(
    $state,
    {
      %$state,
      initial_schema_uri => str(''),
      annotations => [
       superhashof({
          instance_location => '',
          keyword_location => '/prefixItems',
          annotation => true,
        }),
      ],
      errors => [],
    },
    'passing prefixItems: one item is annotated',
  );

  $state = {
    %$initial_state,
    keyword => 'prefixItems',
    annotations => [],
    errors => [],
  };

  ok(
    !$state->{vocabularies}[0]->_eval_keyword_prefixItems(
      [ 1, 5, 9 ],
      { prefixItems => [ { title => 'hi', maximum => 3 }, { title => 'hi', maximum => 3 } ] },
      $state),
    'two items, one failing',
  );

  cmp_result(
    $state,
    {
      %$state,
      initial_schema_uri => str(''),
      annotations => [
        superhashof({
          instance_location => '/0',
          keyword_location => '/prefixItems/0/title',
          annotation => 'hi',
        }),
        superhashof({
          instance_location => '',
          keyword_location => '/prefixItems',
          annotation => 1,
        }),
      ],
      errors => [
        methods(TO_JSON => {
          instanceLocation => '/1',
          keywordLocation => '/prefixItems/1/maximum',
          error => 'value is greater than 3',
        }),
        methods(TO_JSON => {
          instanceLocation => '',
          keywordLocation => '/prefixItems',
          error => 'not all items are valid',
        }),
      ],
    },
    'failing prefixItems still collects annotations',
  );
};

subtest 'schema-items' => sub {
  my $state = {
    %$initial_state,
    keyword => 'items',
    annotations => [],
    errors => [],
  };

  ok(
    $state->{vocabularies}[0]->_eval_keyword_items([], { items => true }, $state),
    'no items means that "items" succeeds',
  );

  cmp_result(
    $state,
    my $new_state = {
      %$state,
      initial_schema_uri => str(''),
      annotations => [],
      errors => [],
    },
    'no items: no annotation is produced by items',
  );

  $state = {
    %$initial_state,
    keyword => 'items',
    annotations => [],
    errors => [],
  };

  ok(
    $state->{vocabularies}[0]->_eval_keyword_items([ 1 ], { items => true }, $state),
    'one item',
  );

  cmp_result(
    $state,
    {
      %$state,
      initial_schema_uri => str(''),
      annotations => [
        superhashof({
          instance_location => '',
          keyword_location => '/items',
          annotation => true,
        }),
      ],
      errors => [],
    },
    'passing items: one item is annotated',
  );

  $state = {
    %$initial_state,
    keyword => 'items',
    annotations => [],
    errors => [],
  };

  ok(
    !$state->{vocabularies}[0]->_eval_keyword_items(
      [ 1, 5 ],
      { items => { title => 'hi', maximum => 3 } },
      $state),
    'two items, one failing',
  );

  cmp_result(
    $state,
    {
      %$state,
      initial_schema_uri => str(''),
      annotations => [
        superhashof({
          instance_location => '/0',
          keyword_location => '/items/title',
          annotation => 'hi',
        }),
        superhashof({
          instance_location => '',
          keyword_location => '/items',
          annotation => true,
        }),
      ],
      errors => [
        methods(TO_JSON => {
          instanceLocation => '/1',
          keywordLocation => '/items/maximum',
          error => 'value is greater than 3',
        }),
        methods(TO_JSON => {
          instanceLocation => '',
          keywordLocation => '/items',
          error => 'subschema is not valid against all items',
        }),
      ],
    },
    'failing items still collects annotations',
  );
};

subtest 'additionalItems' => sub {
  my $state = {
    %$initial_state,
    keyword => 'additionalItems',
    annotations => [],
    errors => [],
  };

  ok(
    $state->{vocabularies}[0]->_eval_keyword_items([], { additionalItems => true }, $state),
    'no items means that "additionalItems" succeeds',
  );

  cmp_result(
    $state,
    my $new_state = {
      %$state,
      initial_schema_uri => str(''),
      annotations => [],
      errors => [],
    },
    'no items: no annotation is produced by additionaltems',
  );

  $state = {
    %$initial_state,
    keyword => 'additionalItems',
    annotations => [],
    errors => [],
  };

  ok(
    $state->{vocabularies}[0]->_eval_keyword_additionalItems([ 1 ], { additionalItems => false }, $state),
    'one item',
  );

  cmp_result(
    $state,
    {
      %$state,
      initial_schema_uri => str(''),
      annotations => [],
      errors => [],
    },
    'additionalItems does nothing without items',
  );
};

subtest 'properties' => sub {
  my $state = {
    %$initial_state,
    keyword => 'properties',
    annotations => [],
    errors => [],
  };

  ok(
    $state->{vocabularies}[0]->_eval_keyword_properties({}, { properties => { foo => true } }, $state),
    'no items means that "properties" succeeds',
  );

  cmp_result(
    $state,
    my $new_state = {
      %$state,
      initial_schema_uri => str(''),
      annotations => [
        superhashof({
          instance_location => '',
          keyword_location => '/properties',
          annotation => [],
        }),
      ],
      errors => [],
    },
    'no properties: annotation is still produced by properties',
  );

  $state = {
    %$initial_state,
    keyword => 'properties',
    annotations => [],
    errors => [],
  };

  ok(
    $state->{vocabularies}[0]->_eval_keyword_properties({ foo => 1 }, { properties => { foo => true } }, $state),
    'one property',
  );

  cmp_result(
    $state,
    {
      %$state,
      initial_schema_uri => str(''),
      annotations => [
        superhashof({
          instance_location => '',
          keyword_location => '/properties',
          annotation => [ 'foo' ],
        }),
      ],
      errors => [],
    },
    'passing properties: one property is annotated',
  );

  $state = {
    %$initial_state,
    keyword => 'properties',
    annotations => [],
    errors => [],
  };

  ok(
    !$state->{vocabularies}[0]->_eval_keyword_properties(
      { foo => 1, bar => 5 },
      { properties => {
          foo => { title => 'hi', maximum => 3 },
          bar => { title => 'hi', maximum => 3 },
        },
      },
      $state),
    'two properties, one failing',
  );

  cmp_result(
    $state,
    {
      %$state,
      initial_schema_uri => str(''),
      annotations => [
        superhashof({
          instance_location => '/foo',
          keyword_location => '/properties/foo/title',
          annotation => 'hi',
        }),
        superhashof({
          instance_location => '',
          keyword_location => '/properties',
          annotation => [ qw(bar foo) ],
        }),
      ],
      errors => [
        methods(TO_JSON => {
          instanceLocation => '/bar',
          keywordLocation => '/properties/bar/maximum',
          error => 'value is greater than 3',
        }),
        methods(TO_JSON => {
          instanceLocation => '',
          keywordLocation => '/properties',
          error => 'not all properties are valid',
        }),
      ],
    },
    'failing properties still collects annotations',
  );
};

subtest 'patternProperties' => sub {
  my $state = {
    %$initial_state,
    keyword => 'patternProperties',
    annotations => [],
    errors => [],
  };

  ok(
    $state->{vocabularies}[0]->_eval_keyword_patternProperties({}, { patternProperties => { foo => true } }, $state),
    'no items means that "patternProperties" succeeds',
  );

  cmp_result(
    $state,
    my $new_state = {
      %$state,
      initial_schema_uri => str(''),
      annotations => [
        superhashof({
          instance_location => '',
          keyword_location => '/patternProperties',
          annotation => [],
        }),
      ],
      errors => [],
    },
    'no pProperties: annotation is still produced by patternProperties',
  );

  $state = {
    %$initial_state,
    keyword => 'patternProperties',
    annotations => [],
    errors => [],
  };

  ok(
    $state->{vocabularies}[0]->_eval_keyword_patternProperties({ foo => 1 }, { patternProperties => { foo => true } }, $state),
    'one property',
  );

  cmp_result(
    $state,
    {
      %$state,
      initial_schema_uri => str(''),
      annotations => [
        superhashof({
          instance_location => '',
          keyword_location => '/patternProperties',
          annotation => [ 'foo' ],
        }),
      ],
      errors => [],
    },
    'passing properties: one property is annotated',
  );

  $state = {
    %$initial_state,
    keyword => 'patternProperties',
    annotations => [],
    errors => [],
  };

  ok(
    !$state->{vocabularies}[0]->_eval_keyword_patternProperties(
      { foo => 1, bar => 5 },
      { patternProperties => {
          foo => { title => 'hi', maximum => 3 },
          bar => { title => 'hi', maximum => 3 },
        },
      },
      $state),
    'two properties, one failing',
  );

  cmp_result(
    $state,
    {
      %$state,
      initial_schema_uri => str(''),
      annotations => [
        superhashof({
          instance_location => '/foo',
          keyword_location => '/patternProperties/foo/title',
          annotation => 'hi',
        }),
        superhashof({
          instance_location => '',
          keyword_location => '/patternProperties',
          annotation => [ qw(bar foo) ],
        }),
      ],
      errors => [
        methods(TO_JSON => {
          instanceLocation => '/bar',
          keywordLocation => '/patternProperties/bar/maximum',
          error => 'value is greater than 3',
        }),
        methods(TO_JSON => {
          instanceLocation => '',
          keywordLocation => '/patternProperties',
          error => 'not all properties are valid',
        }),
      ],
    },
    'failing patternProperties still collects annotations',
  );
};

subtest 'additionalProperties' => sub {
  my $state = {
    %$initial_state,
    keyword => 'additionalProperties',
    annotations => [],
    errors => [],
  };

  ok(
    $state->{vocabularies}[0]->_eval_keyword_additionalProperties([], { additionalProperties => true }, $state),
    'no items means that "additionalProperties" succeeds',
  );

  cmp_result(
    $state,
    my $new_state = {
      %$state,
      initial_schema_uri => str(''),
      annotations => [],
      errors => [],
    },
    'no properties: no annotation is produced by additionalProperties',
  );

  $state = {
    %$initial_state,
    keyword => 'additionalProperties',
    annotations => [],
    errors => [],
  };

  ok(
    $state->{vocabularies}[0]->_eval_keyword_additionalProperties({ foo => 1 }, { additionalProperties => true }, $state),
    'one property',
  );

  cmp_result(
    $state,
    {
      %$state,
      initial_schema_uri => str(''),
      annotations => [
        superhashof({
          instance_location => '',
          keyword_location => '/additionalProperties',
          annotation => [ 'foo' ],
        }),
      ],
      errors => [],
    },
    'passing additionalProperties: one property is annotated',
  );

  $state = {
    %$initial_state,
    keyword => 'additionalProperties',
    annotations => [],
    errors => [],
  };

  ok(
    !$state->{vocabularies}[0]->_eval_keyword_additionalProperties(
      { foo => 1, bar => 3, baz => 5 },
      {
        properties => { foo => true },
        additionalProperties => { title => 'hi', maximum => 3 },
      },
      $state),
    'two properties, one failing',
  );

  cmp_result(
    $state,
    {
      %$state,
      initial_schema_uri => str(''),
      annotations => [
        superhashof({
          instance_location => '/bar',
          keyword_location => '/additionalProperties/title',
          annotation => 'hi',
        }),
        superhashof({
          instance_location => '',
          keyword_location => '/additionalProperties',
          annotation => [ qw(bar baz) ],
        }),
      ],
      errors => [
        methods(TO_JSON => {
          instanceLocation => '/baz',
          keywordLocation => '/additionalProperties/maximum',
          error => 'value is greater than 3',
        }),
        methods(TO_JSON => {
          instanceLocation => '',
          keywordLocation => '/additionalProperties',
          error => 'not all additional properties are valid',
        }),
      ],
    },
    'failing properties still collects annotations',
  );
};

subtest 'unevaluatedProperties' => sub {
  my $state = {
    %$initial_state,
    keyword => 'unevaluatedProperties',
    annotations => [],
    errors => [],
  };

  ok(
    $state->{vocabularies}[0]->_eval_keyword_unevaluatedProperties([], { unevaluatedProperties => true }, $state),
    'no items means that "unevaluatedProperties" succeeds',
  );

  cmp_result(
    $state,
    my $new_state = {
      %$state,
      initial_schema_uri => str(''),
      annotations => [],
      errors => [],
    },
    'no properties: no annotation is produced by unevaluatedProperties',
  );

  $state = {
    %$initial_state,
    keyword => 'unevaluatedProperties',
    annotations => [],
    errors => [],
  };

  ok(
    $state->{vocabularies}[0]->_eval_keyword_unevaluatedProperties({ foo => 1 }, { unevaluatedProperties => true }, $state),
    'one property',
  );

  cmp_result(
    $state,
    {
      %$state,
      initial_schema_uri => str(''),
      annotations => [
        superhashof({
          instance_location => '',
          keyword_location => '/unevaluatedProperties',
          annotation => [ 'foo' ],
        }),
      ],
      errors => [],
    },
    'passing unevaluatedProperties: one property is annotated',
  );

  $state = {
    %$initial_state,
    keyword => 'unevaluatedProperties',
    annotations => [],
    errors => [],
  };

  ok(
    !$state->{vocabularies}[0]->_eval_keyword_unevaluatedProperties(
      { foo => 1, bar => 3, baz => 5 },
      {
        properties => { foo => true },
        unevaluatedProperties => { title => 'hi', maximum => 3 },
      },
      $state),
    'two properties, one failing',
  );

  cmp_result(
    $state,
    {
      %$state,
      initial_schema_uri => str(''),
      annotations => [
        (map superhashof({
          instance_location => '/'.$_,
          keyword_location => '/unevaluatedProperties/title',
          annotation => 'hi',
        }), qw(bar foo)),
        superhashof({
          instance_location => '',
          keyword_location => '/unevaluatedProperties',
          annotation => [ qw(bar baz foo) ],
        }),
      ],
      errors => [
        methods(TO_JSON => {
          instanceLocation => '/baz',
          keywordLocation => '/unevaluatedProperties/maximum',
          error => 'value is greater than 3',
        }),
        methods(TO_JSON => {
          instanceLocation => '',
          keywordLocation => '/unevaluatedProperties',
          error => 'not all additional properties are valid',
        }),
      ],
    },
    'failing unevaluatedProperties still collects annotations',
  );
};

subtest 'collect_annotations and unevaluated keywords' => sub {
  my $js = JSON::Schema::Modern->new(collect_annotations => 0);

  cmp_result(
    $js->evaluate(
      [ 1 ],
      my $schema = {
        '$id' => 'unevaluatedItems.json',
        prefixItems => [ true ],
        unevaluatedItems => false,
      },
    )->TO_JSON,
    { valid => true },
    'when "collect_annotations" is explicitly set to false, unevaluatedItems can still be used (valid result, no annotations in result)',
  );

  cmp_result(
    $js->evaluate(
      [ 1, 2 ],
      $schema,
    )->TO_JSON,
    {
      valid => false,
      errors => [
        {
          instanceLocation => '/1',
          keywordLocation => '/unevaluatedItems',
          absoluteKeywordLocation => 'unevaluatedItems.json#/unevaluatedItems',
          error => 'additional item not permitted',
        },
        {
          instanceLocation => '',
          keywordLocation => '/unevaluatedItems',
          absoluteKeywordLocation => 'unevaluatedItems.json#/unevaluatedItems',
          error => 'subschema is not valid against all additional items',
        },
      ],
    },
    'when "collect_annotations" is explicitly set to false, unevaluatedItems can still be used (invalid result)',
  );

  cmp_result(
    $js->evaluate(
      { foo => 1 },
      $schema = {
        '$id' => 'unevaluatedProperties.json',
        properties => { foo => true },
        unevaluatedProperties => false,
      },
    )->TO_JSON,
    { valid => true },
    'when "collect_annotations" is explicitly set to false, unevaluatedProperties can still be used (valid result, no annotations)',
  );
  cmp_result(
    $js->evaluate(
      { foo => 1, bar => 2 },
      $schema,
    )->TO_JSON,
    {
      valid => false,
      errors => [
        {
          instanceLocation => '/bar',
          keywordLocation => '/unevaluatedProperties',
          absoluteKeywordLocation => 'unevaluatedProperties.json#/unevaluatedProperties',
          error => 'additional property not permitted',
        },
        {
          instanceLocation => '',
          keywordLocation => '/unevaluatedProperties',
          absoluteKeywordLocation => 'unevaluatedProperties.json#/unevaluatedProperties',
          error => 'not all additional properties are valid',
        },
      ],
    },
    'when "collect_annotations" is explicitly set to false, unevaluatedProperties can still be used (invalid result)',
  );

  cmp_result(
    $js->evaluate(
      {
        item => [ 1 ],
        property => { foo => 1 },
      },
      $schema = {
        properties => {
          item => { '$ref' => 'unevaluatedItems.json' },
          property => { '$ref' => 'unevaluatedProperties.json' },
        },
      },
    )->TO_JSON,
    { valid => true },
    'when "collect_annotations" is explicitly set to false, unevaluatedProperties still be used, even in other documents (valid result)',
  );

  cmp_result(
    $js->evaluate(
      {
        item => [ 1, 2 ],
        property => { foo => 1, bar => 2 },
      },
      $schema,
    )->TO_JSON,
    {
      valid => false,
      errors => [
        {
          instanceLocation => '/item/1',
          keywordLocation => '/properties/item/$ref/unevaluatedItems',
          absoluteKeywordLocation => 'unevaluatedItems.json#/unevaluatedItems',
          error => 'additional item not permitted',
        },
        {
          instanceLocation => '/item',
          keywordLocation => '/properties/item/$ref/unevaluatedItems',
          absoluteKeywordLocation => 'unevaluatedItems.json#/unevaluatedItems',
          error => 'subschema is not valid against all additional items',
        },
        {
          instanceLocation => '/property/bar',
          keywordLocation => '/properties/property/$ref/unevaluatedProperties',
          absoluteKeywordLocation => 'unevaluatedProperties.json#/unevaluatedProperties',
          error => 'additional property not permitted',
        },
        {
          instanceLocation => '/property',
          keywordLocation => '/properties/property/$ref/unevaluatedProperties',
          absoluteKeywordLocation => 'unevaluatedProperties.json#/unevaluatedProperties',
          error => 'not all additional properties are valid',
        },
        {
          instanceLocation => '',
          keywordLocation => '/properties',
          error => 'not all properties are valid',
        },
      ],
    },
    'when "collect_annotations" is explicitly set to false, unevaluatedProperties still be used, even in other documents (invalid result)',
  );


  $js = JSON::Schema::Modern->new(collect_annotations => 1);

  cmp_result(
    $js->evaluate(
      [ 1 ],
      {
        prefixItems => [ true ],
        unevaluatedItems => false,
      },
    )->TO_JSON,
    {
      valid => true,
      annotations => [
        {
          instanceLocation => '',
          keywordLocation => '/prefixItems',
          annotation => true,
        },
      ],
    },
    'when "collect_annotations" is set to true, unevaluatedItems works, and annotations are returned',
  );

  cmp_result(
    $js->evaluate(
      { foo => 1 },
      {
        properties => { foo => true },
        unevaluatedProperties => false,
      },
    )->TO_JSON,
    {
      valid => true,
      annotations => [
        {
          instanceLocation => '',
          keywordLocation => '/properties',
          annotation => [ 'foo' ],
        },
        {
          instanceLocation => '',
          keywordLocation => '/unevaluatedProperties',
          annotation => [],
        },
      ],
    },
    'when "collect_annotations" is set to true, unevaluatedProperties passes, and annotations are returned',
  );

  $js = JSON::Schema::Modern->new;

  cmp_result(
    $js->evaluate(
      [ 1 ],
      {
        '$id' => 'unevaluatedItems.json',
        prefixItems => [ true ],
        unevaluatedItems => false,
      },
    )->TO_JSON,
    {
      valid => true,
    },
    'when "collect_annotations" is not set, unevaluatedItems still works, but annotations are not returned',
  );

  cmp_result(
    $js->evaluate(
      { foo => 1 },
      {
        '$id' => 'unevaluatedProperties.json',
        properties => { foo => true },
        unevaluatedProperties => false,
      },
    )->TO_JSON,
    {
      valid => true,
    },
    'when "collect_annotations" is not set, unevaluatedProperties still works, but annotations are not returned',
  );

  cmp_result(
    $js->evaluate(
      {
        item => [ 1 ],
        property => { foo => 1 },
      },
      {
        properties => {
          item => { '$ref' => 'unevaluatedItems.json' },
          property => { '$ref' => 'unevaluatedProperties.json' },
        },
      },
    )->TO_JSON,
    {
      valid => true,
    },
    '... still works when unevaluated keywords are in a separate document',
  );

  my $doc_items = $js->add_schema('prefixItems.json', { prefixItems => [ true ] });

  my $doc_properties = $js->add_schema('properties.json', { properties => { foo => true } });

  cmp_result(
    $js->evaluate(
      {
        item => [ 1 ],
        property => { foo => 1 },
      },
      {
        properties => {
          item => {
            '$ref' => 'prefixItems.json',
            unevaluatedItems => false,
          },
          property => {
            '$ref' => 'properties.json',
            unevaluatedProperties => false,
          },
        },
      },
    )->TO_JSON,
    {
      valid => true,
    },
    'referenced schemas still produce annotations internally when needed, even when not required to evaluate themselves in isolation',
  );
};

subtest 'annotate unknown keywords' => sub {
  my $data = {
    item => [ 1 ],
    property => { foo => 1 },
  };
  my $schema = {
    properties => {
      item => {
        items => true,
        unevaluatedItems => false,
        bloop => 5,
      },
      property => {
        properties => { foo => true },
        unevaluatedProperties => false,
        blap => { hi => 1 },
      },
    },
    blip => [ 1, 2, 3 ],
  };

  cmp_result(
    JSON::Schema::Modern->new->evaluate(
      $data,
      $schema,
    )->TO_JSON,
    {
      valid => true,
    },
    'no annotations even when collect_annotations is false',
  );

  cmp_result(
    (my $result = JSON::Schema::Modern->new(collect_annotations => 1)->evaluate(
      $data,
      $schema,
    ))->TO_JSON,
    {
      valid => true,
      annotations => [
        {
          instanceLocation => '/item',
          keywordLocation => '/properties/item/items',
          annotation => true,
        },
        {
          instanceLocation => '/item',
          keywordLocation => '/properties/item/bloop',
          annotation => 5,
        },
        {
          instanceLocation => '/property',
          keywordLocation => '/properties/property/properties',
          annotation => [ 'foo' ],
        },
        {
          instanceLocation => '/property',
          keywordLocation => '/properties/property/unevaluatedProperties',
          annotation => [],
        },
        {
          instanceLocation => '/property',
          keywordLocation => '/properties/property/blap',
          annotation => { hi => 1 },
        },
        {
          instanceLocation => '',
          keywordLocation => '/properties',
          annotation => [ 'item', 'property' ],
        },
        {
          instanceLocation => '',
          keywordLocation => '/blip',
          annotation => [ 1, 2, 3 ],
        },
      ],
    },
    'unknown keywords are collected as annotations',
  );

  cmp_result(
    [ $result->annotations ],
    [
      methods(keyword => 'items', unknown => bool(0)),
      methods(keyword => 'bloop', unknown => bool(1)),
      methods(keyword => 'properties', unknown => bool(0)),
      methods(keyword => 'unevaluatedProperties', unknown => bool(0)),
      methods(keyword => 'blap', unknown => bool(1)),
      methods(keyword => 'properties', unknown => bool(0)),
      methods(keyword => 'blip', unknown => bool(1)),
    ],
    '"unknown" keyword is set on the annotation objects for unknown keywords',
  );

  cmp_result(
    $result = JSON::Schema::Modern->new(specification_version => 'draft2019-09', collect_annotations => 1)
        ->evaluate(
      $data,
      $schema,
    )->TO_JSON,
    {
      valid => true,
      annotations => [
        {
          instanceLocation => '/item',
          keywordLocation => '/properties/item/items',
          annotation => true,
        },
        # no bloop
        {
          instanceLocation => '/property',
          keywordLocation => '/properties/property/properties',
          annotation => [ 'foo' ],
        },
        {
          instanceLocation => '/property',
          keywordLocation => '/properties/property/unevaluatedProperties',
          annotation => [],
        },
        # no blap
        {
          instanceLocation => '',
          keywordLocation => '/properties',
          annotation => [ 'item', 'property' ],
        },
        # no blip
      ],
    },
    'no annotations from unknown keywords in draft2019-09',
  );
};

subtest 'items + additionalItems, prefixItems + items' => sub {
  cmp_result(
    JSON::Schema::Modern->new(specification_version => 'draft2019-09', collect_annotations => 1)
        ->evaluate(
      [ 1, 2, 3 ],
      {
        items => { maximum => 5 },
        additionalItems => { maximum => 0 },
      }
    )->TO_JSON,
    {
      valid => true,
      annotations => [
        {
          instanceLocation => '',
          keywordLocation => '/items',
          annotation => true,
        },
        # no error nor annotation from additionalItems
      ],
    },
    'schema-based items + additionalItems',
  );

  cmp_result(
    my $result = JSON::Schema::Modern->new(collect_annotations => 1)->evaluate(
      [ 1, 2, 3 ],
      {
        prefixItems => [ { maximum => 5 }, { maximum => 5 }, { maximum => 5 } ],
        items => { maximum => 0 },
      }
    )->TO_JSON,
    {
      valid => true,
      annotations => [
        {
          instanceLocation => '',
          keywordLocation => '/prefixItems',
          annotation => true,
        },
        # no error nor annotation from items
      ],
    },
    'prefixItems + schema-based items',
  );
};

done_testing;
