# vim: set ts=8 sts=2 sw=2 tw=100 et :
use strictures 2;
use 5.020;
use experimental qw(signatures postderef);
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';
use utf8;
use open ':std', ':encoding(UTF-8)'; # force stdin, stdout, stderr into utf8

use Test::More;
use Test::Deep;
use Test::Fatal;
use OpenAPI::Modern;
use JSON::Schema::Modern::Utilities 'jsonp';
use Test::File::ShareDir -share => { -dist => { 'OpenAPI-Modern' => 'share' } };
use constant { true => JSON::PP::true, false => JSON::PP::false };
use YAML::PP 0.005;

use lib 't/lib';
use Helper;

my $openapi_preamble = <<'YAML';
---
openapi: 3.1.0
info:
  title: Test API
  version: 1.2.3
YAML

# the absolute uri we will see in errors
my $doc_uri_rel = Mojo::URL->new('/api');
my $doc_uri = $doc_uri_rel->to_abs(Mojo::URL->new('https://example.com'));
my $yamlpp = YAML::PP->new(boolean => 'JSON::PP');

my $type_index = 0;

START:
$::TYPE = $::TYPES[$type_index];
note 'REQUEST/RESPONSE TYPE: '.$::TYPE;

subtest 'request is parsed to get path information' => sub {
  my $openapi = OpenAPI::Modern->new(
    openapi_uri => '/api',
    openapi_schema => $yamlpp->load_string(<<YAML));
$openapi_preamble
paths:
  /foo/{foo_id}:
    post:
      operationId: my-post-path
  /foo/bar:
    get:
      operationId: my-get-path
webhooks:
  my_hook:
    description: I like webhooks
    post:
      operationId: hooky
YAML

  my $request = request('GET', 'http://example.com/foo/bar');
  ok(!$openapi->find_path(my $options = { request => $request, path_template => '/foo/baz', path_captures => {} }),
    'find_path returns false');
  cmp_deeply(
    $options,
    {
      request => isa('Mojo::Message::Request'),
      path_template => '/foo/baz',
      path_captures => {},
      method => 'get',
      errors => [
        methods(TO_JSON => {
          instanceLocation => '/request/uri/path',
          keywordLocation => '/paths',
          absoluteKeywordLocation => $doc_uri->clone->fragment('/paths')->to_string,
          error => 'missing path-item "/foo/baz"',
        }),
      ],
    },
    'unsuccessful path extraction results in the error being returned in the options hash',
  );


  ok(!$openapi->find_path($options = { request => $request, operation_id => 'bloop', path_captures => {} }),
    'find_path returns false');
  cmp_deeply(
    $options,
    {
      request => isa('Mojo::Message::Request'),
      method => 'get',
      path_captures => {},
      operation_id => 'bloop',
      errors => [
        methods(TO_JSON => {
          instanceLocation => '/request/uri/path',
          keywordLocation => jsonp('/paths'),
          absoluteKeywordLocation => $doc_uri->clone->fragment(jsonp('/paths'))->to_string,
          error => 'unknown operation_id "bloop"',
        }),
      ],
    },
    'path template does not exist under /paths',
  );


  ok(!$openapi->find_path($options = { request => $request, operation_id => 'hooky', path_captures => {} }),
    'find_path returns false');
  cmp_deeply(
    $options,
    {
      request => isa('Mojo::Message::Request'),
      method => 'get',
      path_captures => {},
      operation_id => 'hooky',
      errors => [
        methods(TO_JSON => {
          instanceLocation => '/request/uri/path',
          keywordLocation => '/webhooks/my_hook/post/operationId',
          absoluteKeywordLocation => $doc_uri->clone->fragment(jsonp('/webhooks/my_hook/post/operationId'))->to_string,
          error => 'operation id does not have an associated path',
        }),
      ],
    },
    'path template does not exist under /paths',
  );


  ok(!$openapi->find_path($options = { request => request('GET', 'http://example.com/foo/bloop') }),
    'find_path returns false');
  cmp_deeply(
    $options,
    {
      request => isa('Mojo::Message::Request'),
      method => 'get',
      path_template => '/foo/{foo_id}',
      path_captures => { foo_id => 'bloop' },
      errors => [
        methods(TO_JSON => {
          instanceLocation => '/request/method',
          keywordLocation => jsonp(qw(/paths /foo/{foo_id} get)),
          absoluteKeywordLocation => $doc_uri->clone->fragment(jsonp(qw(/paths /foo/{foo_id} get)))->to_string,
          error => 'missing operation for HTTP method "get"',
        }),
      ],
    },
    'operation does not exist under /paths/<path_template>/<method>',
  );


  ok(!$openapi->find_path($options = { request => request('POST', 'http://example.com/foo/bar'),
      path_template => '/foo/{foo_id}', operation_id => 'my-get-path', path_captures => {} }),
    'find_path returns false');
  cmp_deeply(
    $options,
    {
      request => isa('Mojo::Message::Request'),
      method => 'post',
      path_template => '/foo/{foo_id}',
      path_captures => {},
      operation_id => 'my-get-path',
      errors => [
        methods(TO_JSON => {
          instanceLocation => '/request/uri/path',
          keywordLocation => '/paths/~1foo~1bar',
          absoluteKeywordLocation => $doc_uri->clone->fragment(jsonp('/paths', qw(/foo/bar)))->to_string,
          error => 'operation does not match provided path_template',
        }),
      ],
    },
    'path_template and operation_id are inconsistent',
  );


  ok(!$openapi->find_path($options = { request => request('POST', 'http://example.com/foo/bar'),
      operation_id => 'my-get-path', path_captures => {} }),
    'find_path returns false');
  cmp_deeply(
    $options,
    {
      request => isa('Mojo::Message::Request'),
      method => 'post',
      path_captures => {},
      operation_id => 'my-get-path',
      errors => [
        methods(TO_JSON => {
          instanceLocation => '/request/method',
          keywordLocation => '/paths/~1foo~1bar/get',
          absoluteKeywordLocation => $doc_uri->clone->fragment(jsonp('/paths', qw(/foo/bar get)))->to_string,
          error => 'wrong HTTP method post',
        }),
      ],
    },
    'request HTTP method does not match operation',
  );


  ok(!$openapi->find_path($options = { request => request('POST', 'http://example.com/foo/bar'), method => 'GET'}),
    'find_path returns false');
  cmp_deeply(
    $options,
    {
      request => isa('Mojo::Message::Request'),
      method => 'GET',
      errors => [
        methods(TO_JSON => {
          instanceLocation => '/request/method',
          keywordLocation => '',
          absoluteKeywordLocation => $doc_uri->clone->to_string,
          error => 'wrong HTTP method POST',
        }),
      ],
    },
    'request HTTP method does not match method option',
  );


  ok(!$openapi->find_path($options = { request => request('POST', 'http://example.com/foo/bar'),
        path_template => '/foo/{foo_id}', path_captures => { bloop => 'bar' } }),
    'find_path returns false');
  cmp_deeply(
    $options,
    {
      request => isa('Mojo::Message::Request'),
      method => 'post',
      path_template => '/foo/{foo_id}',
      path_captures => { bloop => 'bar' },
      operation_path => '/paths/~1foo~1{foo_id}/post',
      errors => [
        methods(TO_JSON => {
          instanceLocation => '/request/uri/path',
          keywordLocation => '/paths/~1foo~1{foo_id}',
          absoluteKeywordLocation => $doc_uri->clone->fragment(jsonp(qw(/paths /foo/{foo_id})))->to_string,
          error => 'provided path_captures names do not match path template "/foo/{foo_id}"',
        }),
      ],
    },
    'path template does not match path captures',
  );


  ok($openapi->find_path($options = { request => request('GET', 'http://example.com/foo/bar'),
      path_template => '/foo/bar', operation_id => 'my-get-path', path_captures => {} }),
    'find_path returns successfully');
  cmp_deeply(
    $options,
    {
      request => isa('Mojo::Message::Request'),
      method => 'get',
      path_template => '/foo/bar',
      path_captures => {},
      operation_id => 'my-get-path',
      operation_path => '/paths/~1foo~1bar/get',
      errors => [],
    },
    'path_template and operation_id can both be passed, if consistent',
  );


  ok(!$openapi->find_path($options = { request => request('GET', 'http://example.com/something/else'),
      path_template => '/foo/bar', path_captures => {} }),
    'find_path returns false');
  cmp_deeply(
    $options,
    {
      request => isa('Mojo::Message::Request'),
      method => 'get',
      path_template => '/foo/bar',
      path_captures => {},
      operation_path => '/paths/~1foo~1bar/get',
      errors => [
        methods(TO_JSON => {
          instanceLocation => '/request/uri/path',
          keywordLocation => jsonp(qw(/paths /foo/bar)),
          absoluteKeywordLocation => $doc_uri->clone->fragment(jsonp(qw(/paths /foo/bar)))->to_string,
          error => 'provided path_template does not match request URI',
        }),
      ],
    },
    'path_template is not consistent with request URI, with no captures',
  );


  ok(!$openapi->find_path($options = { request => request('POST', 'http://example.com/something/else'),
      path_template => '/foo/{foo_id}', path_captures => { foo_id => 123 } }),
    'find_path returns false');
  cmp_deeply(
    $options,
    {
      request => isa('Mojo::Message::Request'),
      method => 'post',
      path_template => '/foo/{foo_id}',
      path_captures => { foo_id => 123 },
      operation_path => '/paths/~1foo~1{foo_id}/post',
      errors => [
        methods(TO_JSON => {
          instanceLocation => '/request/uri/path',
          keywordLocation => jsonp(qw(/paths /foo/{foo_id})),
          absoluteKeywordLocation => $doc_uri->clone->fragment(jsonp(qw(/paths /foo/{foo_id})))->to_string,
          error => 'provided path_template does not match request URI',
        }),
      ],
    },
    'path_template is not consistent with request URI, with captures',
  );


  ok(!$openapi->find_path($options = { request => request('POST', 'http://example.com/something/else'),
      path_template => '/foo/{foo_id}' }),
    'find_path returns false');
  cmp_deeply(
    $options,
    {
      request => isa('Mojo::Message::Request'),
      method => 'post',
      path_template => '/foo/{foo_id}',
      operation_path => '/paths/~1foo~1{foo_id}/post',
      errors => [
        methods(TO_JSON => {
          instanceLocation => '/request/uri/path',
          keywordLocation => jsonp(qw(/paths /foo/{foo_id})),
          absoluteKeywordLocation => $doc_uri->clone->fragment(jsonp(qw(/paths /foo/{foo_id})))->to_string,
          error => 'provided path_template does not match request URI',
        }),
      ],
    },
    'path_template is not consistent with request URI, captures not provided',
  );


  ok(!$openapi->find_path($options = { request => request('GET', 'http://example.com/something/else'),
      operation_id => 'my-get-path', path_captures => {} }),
    'find_path returns false');
  cmp_deeply(
    $options,
    {
      request => isa('Mojo::Message::Request'),
      method => 'get',
      path_captures => {},
      operation_id => 'my-get-path',
      operation_path => '/paths/~1foo~1bar/get',
      errors => [
        methods(TO_JSON => {
          instanceLocation => '/request/uri/path',
          keywordLocation => jsonp(qw(/paths /foo/bar)),
          absoluteKeywordLocation => $doc_uri->clone->fragment(jsonp(qw(/paths /foo/bar)))->to_string,
          error => 'provided operation_id does not match request URI',
        }),
      ],
    },
    'operation_id is not consistent with request URI',
  );


  ok(!$openapi->find_path($options = { request => request('POST', 'http://example.com/foo/hello'),
      operation_id => 'my-post-path', path_captures => { foo_id => 'goodbye' } }),
    'find_path returns false');
  cmp_deeply(
    $options,
    {
      request => isa('Mojo::Message::Request'),
      method => 'post',
      path_captures => { foo_id => 'goodbye' },
      operation_id => 'my-post-path',
      operation_path => '/paths/~1foo~1{foo_id}/post',
      errors => [
        methods(TO_JSON => {
          instanceLocation => '/request/uri/path',
          keywordLocation => jsonp(qw(/paths /foo/{foo_id})),
          absoluteKeywordLocation => $doc_uri->clone->fragment(jsonp(qw(/paths /foo/{foo_id})))->to_string,
          error => 'provided path_captures values do not match request URI',
        }),
      ],
    },
    'path_captures values are not consistent with request URI',
  );


  $openapi = OpenAPI::Modern->new(
    openapi_uri => '/api',
    openapi_schema => $yamlpp->load_string(<<YAML));
$openapi_preamble
paths:
  /foo: {}
YAML

  ok(!$openapi->find_path($options = { request => request('POST', 'http://example.com/foo/bar'),
      path_template => '/foo', path_captures => {} }),
    'find_path returns false');
  cmp_deeply(
    $options,
    {
      request => isa('Mojo::Message::Request'),
      method => 'post',
      path_template => '/foo',
      path_captures => {},
      errors => [
        methods(TO_JSON => {
          instanceLocation => '/request/method',
          keywordLocation => jsonp(qw(/paths /foo post)),
          absoluteKeywordLocation => $doc_uri->clone->fragment(jsonp(qw(/paths /foo post)))->to_string,
          error => 'missing operation for HTTP method "post"',
        }),
      ],
    },
    'operation does not exist under /paths/<path-template>',
  );


  $openapi = OpenAPI::Modern->new(
    openapi_uri => '/api',
    openapi_schema => $yamlpp->load_string(<<YAML));
$openapi_preamble
paths:
  /foo/{foo_id}:
    get:
      operationId: my-get-path
YAML

  ok($openapi->find_path($options = { request => request('GET', 'http://example.com/foo/123'),
      path_template => '/foo/{foo_id}' }),
    'find_path returns successfully',
  );
  cmp_deeply(
    $options,
    {
      request => isa('Mojo::Message::Request'),
      operation_id => 'my-get-path',
      path_template => '/foo/{foo_id}',
      path_captures => { foo_id => '123' },
      method => 'get',
      operation_path => '/paths/~1foo~1{foo_id}/get',
      errors => [],
    },
    'path capture values and method are extracted from the path template and request uri',
  );


  ok($openapi->find_path($options = { request => request('GET', 'http://example.com/foo/123'),
      operation_id => 'my-get-path' }),
    'find_path returns successfully',
  );
  cmp_deeply(
    $options,
    {
      request => isa('Mojo::Message::Request'),
      operation_id => 'my-get-path',
      path_template => '/foo/{foo_id}',
      path_captures => { foo_id => '123' },
      method => 'get',
      operation_path => '/paths/~1foo~1{foo_id}/get',
      errors => [],
    },
    'path capture values are extracted from the operation id and request uri',
  );


  ok($openapi->find_path($options = { request => request( 'GET', 'http://example.com/foo/123') }),
    'find_path returns successfully');
  cmp_deeply(
    $options,
    {
      request => isa('Mojo::Message::Request'),
      path_template => '/foo/{foo_id}',
      path_captures => { foo_id => '123' },
      method => 'get',
      operation_id => 'my-get-path',
      operation_path => '/paths/~1foo~1{foo_id}/get',
      errors => [],
    },
    'path_item and path_capture variables are returned in the provided options hash',
  );


  ok(!$openapi->find_path($options = { request => request('GET', 'http://example.com/foo/123'),
      path_captures => { foo_id => 'a' } }),
    'find_path returns false');
  cmp_deeply(
    $options,
    {
      request => isa('Mojo::Message::Request'),
      method => 'get',
      path_template => '/foo/{foo_id}',
      path_captures => { foo_id => 'a' },
      errors => [
        methods(TO_JSON => {
          instanceLocation => '/request/uri/path',
          keywordLocation => jsonp('/paths'),
          absoluteKeywordLocation => $doc_uri->clone->fragment(jsonp('/paths'))->to_string,
          error => 'provided path_captures values do not match request URI',
        }),
      ],
    },
    'request URI is inconsistent with provided path captures',
  );


  ok(!$openapi->find_path($options = { request => request('GET', 'http://example.com/bloop/blah') }),
    'find_path returns false');
  cmp_deeply(
    $options,
    {
      request => isa('Mojo::Message::Request'),
      method => 'get',
      errors => [
        methods(TO_JSON => {
          instanceLocation => '/request/uri/path',
          keywordLocation => jsonp('/paths'),
          absoluteKeywordLocation => $doc_uri->clone->fragment(jsonp('/paths'))->to_string,
          error => 'no match found for URI path "/bloop/blah"',
        }),
      ],
    },
    'failure to extract path template and capture values from the request uri',
  );


  my $uri = uri('http://example.com', '', 'foo', 'hello // there ಠ_ಠ!');
  ok($openapi->find_path($options = { request => request('GET', $uri),
      path_template => '/foo/{foo_id}', path_captures => { foo_id => 'hello // there ಠ_ಠ!' } }),
    'find_path returns successfully');
  cmp_deeply(
    $options,
    {
      request => isa('Mojo::Message::Request'),
      method => 'get',
      path_template => '/foo/{foo_id}',
      path_captures => { foo_id => 'hello // there ಠ_ಠ!' },
      operation_id => 'my-get-path',
      operation_path => '/paths/~1foo~1{foo_id}/get',
      errors => [],
    },
    'path capture variables are found to be consistent with the URI when some values are url-escaped',
  );


  ok($openapi->find_path($options = { request => request('GET', $uri) } ), 'find_path returns successfully');
  cmp_deeply(
    $options,
    {
      request => isa('Mojo::Message::Request'),
      operation_id => 'my-get-path',
      path_captures => { foo_id => 'hello // there ಠ_ಠ!' },
      path_template => '/foo/{foo_id}',
      method => 'get',
      operation_path => '/paths/~1foo~1{foo_id}/get',
      errors => [],
    },
    'path captures can be properly extracted from the URI when some values are url-escaped',
  );


  $openapi = OpenAPI::Modern->new(
    openapi_uri => '/api',
    openapi_schema => $yamlpp->load_string(<<YAML));
$openapi_preamble
paths:
  /foo/{foo_id}/bar/{foo_id}:
    get: {}
YAML

  $request = request('GET', 'http://example.com/foo/1/bar/2');
  ok(!$openapi->find_path($options = { request => $request }),
    'find_path returns false');

  cmp_deeply(
    $options,
    {
      request => isa('Mojo::Message::Request'),
      method => 'get',
      path_template => '/foo/{foo_id}/bar/{foo_id}',
      errors => [
        methods(TO_JSON => {
          instanceLocation => '/request/uri/path',
          keywordLocation => jsonp('/paths'),
          absoluteKeywordLocation => $doc_uri->clone->fragment(jsonp('/paths'))->to_string,
          error => 'duplicate path capture name foo_id',
        }),
      ],
    },
    'duplicate capture names are detected',
  );


  $openapi = OpenAPI::Modern->new(
    openapi_uri => '/api',
    openapi_schema => $yamlpp->load_string(<<YAML));
$openapi_preamble
paths:
  /foo/{foo_id}:
    get: {}
YAML

  $request = request('GET', 'http://example.com/foo/bar');
  ok($openapi->find_path($options = { request => $request }),
    'find_path returns successfully');
  cmp_deeply(
    $options,
    {
      request => isa('Mojo::Message::Request'),
      path_template => '/foo/{foo_id}',
      path_captures => { foo_id => 'bar' },
      method => 'get',
      operation_path => '/paths/~1foo~1{foo_id}/get',
      errors => [],
    },
    'no path_template provided; no operation_id is recorded, because one does not exist in the schema document',
  );


  ok($openapi->find_path($options = { request => $request, path_template => '/foo/{foo_id}' }),
    'find_path returns successfully');
  cmp_deeply(
    $options,
    {
      request => isa('Mojo::Message::Request'),
      path_template => '/foo/{foo_id}',
      path_captures => { foo_id => 'bar' },
      method => 'get',
      operation_path => '/paths/~1foo~1{foo_id}/get',
      errors => [],
    },
    'path_template provided; no operation_id is recorded, because one does not exist in the schema document',
  );
};

subtest 'no request is provided: options are relied on as the sole source of truth' => sub {
  my $openapi = OpenAPI::Modern->new(
    openapi_uri => '/api',
    openapi_schema => $yamlpp->load_string(<<YAML));
$openapi_preamble
paths:
  /foo/{foo_id}:
    get:
      operationId: my-get-path
YAML

  like(
    exception { ()= $openapi->find_path(my $options = { path_captures => {} }) },
    qr/^at least one of \$options->\{request\}, \$options->\{method\} and \$options->\{operation_id\} must be provided/,
    'method can only be derived from request or operation_id',
  );

  ok(!$openapi->find_path(my $options = { operation_id => 'my-get-path', method => 'POST', path_captures => {} }),
    'find_path returns false');

  cmp_deeply(
    $options,
    {
      operation_id => 'my-get-path',
      method => 'POST',
      path_captures => {},
      errors => [
        methods(TO_JSON => {
          instanceLocation => '/request/method',
          keywordLocation => jsonp(qw(/paths /foo/{foo_id} get)),
          absoluteKeywordLocation => $doc_uri_rel->clone->fragment(jsonp('/paths', qw(/foo/{foo_id} get)))->to_string,
          error => 'wrong HTTP method POST',
        }),
      ],
    },
    'no request provided; operation method does not match passed-in method',
  );

  like(
    exception { ()= $openapi->find_path($options = { method => 'get', path_captures => {} }) },
    qr/^at least one of \$options->\{request\}, \$options->\{path_template\} and \$options->\{operation_id\} must be provided/,
    'path_template can only be derived from request or operation_id',
  );

  ok(!$openapi->find_path($options = { path_template => '/foo/{foo_id}', path_captures => {}, method => 'get' }), 'find_path failed');
  cmp_deeply(
    $options,
    {
      path_template => '/foo/{foo_id}',
      path_captures => {},
      method => 'get',
      operation_path => '/paths/~1foo~1{foo_id}/get',
      errors => [
        methods(TO_JSON => {
          instanceLocation => '/request/uri/path',
          keywordLocation => '/paths/~1foo~1{foo_id}',
          absoluteKeywordLocation => $doc_uri_rel->clone->fragment(jsonp(qw(/paths /foo/{foo_id})))->to_string,
          error => 'provided path_captures names do not match path template "/foo/{foo_id}"',
        }),
      ],
    },
    'no request provided; path template does not match path captures',
  );

  ok($openapi->find_path($options = { operation_id => 'my-get-path', path_captures => { foo_id => 'a' } }), 'find_path succeeded');
  cmp_deeply(
    $options,
    {
      operation_id => 'my-get-path',
      path_captures => { foo_id => 'a' },
      path_template => '/foo/{foo_id}',
      method => 'get',
      operation_path => '/paths/~1foo~1{foo_id}/get',
      errors => [],
    },
    'no request provided; path_template and method are extracted from operation_id and path_captures',
  );

  ok($openapi->find_path($options = { method => 'get', path_template => '/foo/{foo_id}', path_captures => { foo_id => 'a' } }), 'find_path succeeded');
  cmp_deeply(
    $options,
    {
      operation_id => 'my-get-path',
      path_captures => { foo_id => 'a' },
      path_template => '/foo/{foo_id}',
      method => 'get',
      operation_path => '/paths/~1foo~1{foo_id}/get',
      errors => [],
    },
    'no request provided; operation_id are extracted from method and path_template',
  );


  $openapi = OpenAPI::Modern->new(
    openapi_uri => '/api',
    openapi_schema => $yamlpp->load_string(<<YAML));
$openapi_preamble
paths:
  /foo/{foo_id}:
    get: {}
YAML

  ok($openapi->find_path(
      $options = { method => 'get', path_template => '/foo/{foo_id}', path_captures => { foo_id => 'bar' } }),
    'find_path succeeded');
  cmp_deeply(
    $options,
    {
      path_template => '/foo/{foo_id}',
      path_captures => { foo_id => 'bar' },
      method => 'get',
      operation_path => '/paths/~1foo~1{foo_id}/get',
      errors => [],
    },
    'no operation_id is recorded, because one does not exist in the schema document',
  );
};

goto START if ++$type_index < @::TYPES;

done_testing;
